4  単語頻度の重みづけ

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)

4.1 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)であるなどの違いがあります。

4.2 gibasaによる重みづけ

gibasaはRMeCabにおける単語頻度の重みづけをtidytext::bind_tf_idfと同様のスタイルでおこなうことができる関数gibasa::bind_tf_idf2を提供しています。

RMeCabは以下の単語頻度の重みづけをサポートしています。

  • 局所的重み(TF)
    • tf(索引語頻度)
    • tf2(対数化索引語頻度)
    • tf3(2進重み)
  • 大域的重み(IDF)
    • idf(文書頻度の逆数)
    • idf2(大域的IDF)
    • idf3(確率的IDF)
    • idf4(エントロピー)
  • 正規化
    • norm(コサイン正規化)

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とPOS2まで)を確認しながら単語を数えているようなので、ここでのように原形だけを見て数えた結果とは必ずしも一致しません。

4.3 udpipeによる重みづけ

udpipeを使っても単語頻度とTF-IDFを算出できます。また、udpipe::document_term_frequencies_statisticsでは、TF、IDFとTF-IDFにくわえて、Okapi BM25を計算することができます。

udpipe::document_term_frequencies_statisticsには、パラメータとしてkbを渡すことができます。デフォルト値はそれぞれk=1.2b=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
#>  1:    381 呑む    2 0.5000000 3.639872 1.8199359 1.698658 6.182898
#>  2:    398 決闘    2 0.5000000 4.829456 2.4147280 1.698658 8.203595
#>  3:    864 呑む    2 0.5000000 3.639872 1.8199359 1.698658 6.182898
#>  4:    859 呑む    3 0.3333333 3.639872 1.2132906 1.669563 6.076996
#>  5:     60   ん    2 0.4000000 1.900169 0.7600675 1.652365 3.139773
#>  6:     62   ん    2 0.4000000 1.900169 0.7600675 1.652365 3.139773
#>  7:    233   ん    2 0.4000000 1.900169 0.7600675 1.652365 3.139773
#>  8:    260 飛ぶ    2 0.4000000 5.165928 2.0663713 1.652365 8.535999
#>  9:    428 する    2 0.4000000 1.533619 0.6134476 1.652365 2.534099
#> 10:    521   君    2 0.4000000 4.578142 1.8312566 1.652365 7.564761
#> 11:    742   ん    2 0.4000000 1.900169 0.7600675 1.652365 3.139773

4.4 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についてはこの資料などを参照してください。