Code
dat_txt <-
tibble::tibble(
doc_id = seq_along(audubon::polano) |> as.character(),
text = audubon::polano
) |>
dplyr::mutate(text = audubon::strj_normalize(text))
dat <- gibasa::tokenize(dat_txt, text, doc_id)
dat_count <- dat |>
gibasa::prettify(col_select = c("POS1", "Original")) |>
dplyr::filter(POS1 %in% c("名詞", "動詞", "形容詞")) |>
dplyr::mutate(
doc_id = forcats::fct_drop(doc_id),
token = dplyr::if_else(is.na(Original), token, Original)
) |>
dplyr::count(doc_id, token)
tidytextによる重みづけ
tidytext::bind_tf_idf
を使うと単語頻度からTF-IDFを算出することができます。
dat_count |>
tidytext::bind_tf_idf(token, doc_id, n) |>
dplyr::slice_max(tf_idf, n = 5L)
#> # A tibble: 5 × 6
#> doc_id token n tf idf tf_idf
#> <fct> <chr> <int> <dbl> <dbl> <dbl>
#> 1 139 去年 1 1 6.78 6.78
#> 2 880 ざあい 1 1 6.78 6.78
#> 3 255 あわてる 1 1 6.08 6.08
#> 4 713 こちら 1 1 6.08 6.08
#> 5 288 こいつ 1 1 5.39 5.39
tidytextにおけるTFとIDFは、RMeCabにおけるそれとは採用している計算式が異なるため、計算結果が異なります。TFはRMeCabでは生の索引語頻度(tfの場合)ですが、tidytextでは文書内での相対頻度になります。また、IDFはRMeCabでは対数の底が2であるのに対して、tidytextでは底がexp(1)
であるなどの違いがあります。
gibasaによる重みづけ
gibasaはRMeCabにおける単語頻度の重みづけをtidytext::bind_tf_idf
と同様のスタイルでおこなうことができる関数gibasa::bind_tf_idf2
を提供しています。
RMeCabは以下の単語頻度の重みづけをサポートしています。
- 局所的重み(TF)
- tf(索引語頻度)
- tf2(対数化索引語頻度)
- tf3(2進重み)
- 大域的重み(IDF)
- idf(文書頻度の逆数)
- idf2(大域的IDF)
- idf3(確率的IDF)
- idf4(エントロピー)
- 正規化
gibasaはこれらの重みづけを再実装しています。ただし、tf="tf"
はgibasaでは相対頻度になるため、RMeCabのweight="tf*idf"
に相当する出力を得るには、たとえば次のように計算します。
dat_count |>
gibasa::bind_tf_idf2(token, doc_id, n) |>
dplyr::mutate(
tf_idf = n * idf
) |>
dplyr::slice_max(tf_idf, n = 5L)
#> # A tibble: 6 × 6
#> doc_id token n tf idf tf_idf
#> <fct> <chr> <int> <dbl> <dbl> <dbl>
#> 1 825 勉強 6 0.0638 8.77 52.6
#> 2 826 力 6 0.0667 8.77 52.6
#> 3 822 勉強 5 0.0549 8.77 43.9
#> 4 715 の 11 0.103 3.74 41.2
#> 5 113 鞭 4 0.211 9.77 39.1
#> 6 826 得る 4 0.0444 9.77 39.1
なお、注意点として、RMeCabの単語を数える機能は、品詞情報(POS1)を確認しながら単語を数えているようなので、ここでのように原形だけを見て数えた結果とは必ずしも一致しません。
udpipeによる重みづけ
udpipeを使っても単語頻度とTF-IDFを算出できます。また、udpipe::document_term_frequencies_statistics
では、TF、IDFとTF-IDFにくわえて、Okapi BM25を計算することができます。
udpipe::document_term_frequencies_statistics
には、パラメータとしてk
とb
を渡すことができます。デフォルト値はそれぞれk=1.2
、b=0.5
です。k
の値を大きくすると、単語の出現数の増加に対してBM25の値もより大きくなりやすくなります。 k=1.2
というのは、Elasticsearchでもデフォルト値として採用されている値です。WikipediaやElasticsearchの技術記事によると、k
は[1.2, 2.0]
、b=.75
とした場合に、多くのケースでよい結果が得られるとされています。
dplyrを使っていればあまり意識する必要はないと思いますが、udpipeのこのあたりの関数の戻り値はdata.tableである点に注意してください。
suppressPackageStartupMessages(require(dplyr))
dat |>
gibasa::prettify(col_select = c("POS1", "Original")) |>
dplyr::filter(POS1 %in% c("名詞", "動詞", "形容詞")) |>
dplyr::mutate(
doc_id = forcats::fct_drop(doc_id),
token = dplyr::if_else(is.na(Original), token, Original)
) |>
udpipe::document_term_frequencies(document = "doc_id", term = "token") |>
udpipe::document_term_frequencies_statistics(b = .75) |>
dplyr::slice_max(tf_bm25, n = 5L)
#> doc_id term freq tf idf tf_idf tf_bm25 bm25
#> <fctr> <char> <int> <num> <num> <num> <num> <num>
#> 1: 381 呑む 2 0.5000000 3.639872 1.8199359 1.699130 6.184616
#> 2: 398 決闘 2 0.5000000 4.829456 2.4147280 1.699130 8.205875
#> 3: 864 呑む 2 0.5000000 3.639872 1.8199359 1.699130 6.184616
#> 4: 859 呑む 3 0.3333333 3.639872 1.2132906 1.670247 6.079487
#> 5: 60 ん 2 0.4000000 1.900169 0.7600675 1.652923 3.140834
#> 6: 62 ん 2 0.4000000 1.900169 0.7600675 1.652923 3.140834
#> 7: 233 ん 2 0.4000000 1.900169 0.7600675 1.652923 3.140834
#> 8: 260 飛ぶ 2 0.4000000 5.165928 2.0663713 1.652923 8.538884
#> 9: 428 する 2 0.4000000 1.533619 0.6134476 1.652923 2.534955
#> 10: 521 君 2 0.4000000 4.578142 1.8312566 1.652923 7.567318
#> 11: 742 ん 2 0.4000000 1.900169 0.7600675 1.652923 3.140834
tidyloによる重みづけ
TF-IDFによる単語頻度の重みづけのモチベーションは、索引語のなかでも特定の文書だけに多く出現していて、ほかの文書ではそれほど出現しないような「注目に値する語」を調べることにあります。
こうしたことを実現するための値として、tidyloパッケージでは「重み付きログオッズ(weighted log odds)」を計算することができます。
dat_count |>
tidylo::bind_log_odds(set = doc_id, feature = token, n = n) |>
dplyr::filter(is.finite(log_odds_weighted)) |>
dplyr::slice_max(log_odds_weighted, n = 5L)
#> # A tibble: 5 × 4
#> doc_id token n log_odds_weighted
#> <fct> <chr> <int> <dbl>
#> 1 536 する 1 117.
#> 2 430 する 1 105.
#> 3 824 わたくし 1 102.
#> 4 577 いる 1 94.5
#> 5 465 する 1 94.0
ここで用いているデータは小説を改行ごとに一つの文書と見なしていたため、中には次のような極端に短い文書が含まれています。こうした文書では、直観的にはそれほどレアには思われない単語についてもオッズが極端に高くなってしまっているように見えます。
dat_txt |>
dplyr::filter(doc_id %in% c(430, 536, 577, 824)) |>
dplyr::pull(text)
#> [1] "「承知しました。」"
#> [2] "「起訴するぞ。」"
#> [3] "「きっと遠くでございますわ。もし生きていれば。」"
#> [4] "わたくしは思わずはねあがりました。"
weighted log oddsについてはこの資料などを参照してください。