Appendix A — gibasa・MeCabの使い方

A.1 Posit Cloud(旧・RStudio Cloud)でのgibasaの使い方

Rによるデータ分析を手軽に試したい場合には、Posit Cloud(旧・RStudio Cloud)のようなクラウド環境が便利かもしれません。

一方で、Posit Cloudはユーザー権限しかない環境のため、gibasaを使えるようにするまでにはややコツが要ります。とはいえ、gibasaはRMeCabとは異なり、MeCabのバイナリはなくても使える(辞書とmecabrcがあればよい)ので、RMeCabを使う場合ほど複雑なことをする必要はないはずです。

ここでは、Posit Cloudでgibasaを利用できるようにするための手順を簡単に説明します(RMeCabもあわせて試したいという場合には、MeCabのバイナリを自分でビルドする必要があります。その場合はこの記事などを参考にしてください)。

A.1.1 辞書(ipadic, unidic-lite)の配置

MeCabの辞書は、Terminalタブからpipでインストールできます。ここでは、IPA辞書(ipadic)とunidic-liteをインストールします。

python3 -m pip install ipadic unidic-lite
python3 -c "import ipadic; print('dicdir=' + ipadic.DICDIR);" > ~/.mecabrc

A.1.2 gibasaのインストール

gibasaをインストールします。

install.packages("gibasa")

A.1.3 試すには

うまくいっていると、辞書を指定しない場合はIPA辞書が使われます。unidic-liteはsys_dic引数にフルパスを指定することで使用できます。

gibasa::tokenize("こんにちは")
unidic_lite <- path.expand("~/.local/lib/python3.8/site-packages/unidic_lite/dicdir")
gibasa::tokenize("こんにちは", sys_dic = unidic_lite) |>
  gibasa::prettify(into = gibasa::get_dict_features("unidic26"))

A.2 MeCabの辞書をビルドするには

v1.0.1から、gibasaを使ってMeCabのシステム辞書やユーザー辞書をビルドできるようになりました。以下では、gibasaを使ってMeCabの辞書をビルドする方法を紹介します。

MeCabの辞書は、各行が次のようなデータからなる「ヘッダーなしCSVファイル」を用意して、それらをもとに生成します。 ...の部分は見出し語の品詞情報で、ビルドしたい辞書によって異なります。IPA辞書の場合、品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音をこの通りの順番で記述します。

表層形,左文脈ID,右文脈ID,コスト,...

左文脈IDと右文脈IDは、品詞情報が正確に書かれている場合、空にしておくと自動で補完されます。 しかし、品詞情報を正確に書くには、おそらく当の左文脈IDと右文脈IDを含む出力を確認する必要があるため、ふつうに確認した値で埋めてしまったほうが確実です。

ここでは例として、「月ノ美兎」(ANYCOLOR社が運営する「にじさんじ」所属のVTuberの名前)という語彙を含む文をIPA辞書を使いつつ狙いどおりに解析してみましょう。

A.2.1 必要な品詞情報を確認する

gibasa::posDebugRcppは、与えられた文字列について、MeCabの-aオプションに相当する解析結果(解析結果になりえるすべての形態素の組み合わせ)を出力する関数です。 ここでの最適解(is_best == "01")である結果について確認すると、「月ノ美兎」という語彙は次のように複数の形態素に分割されてしまっていることがわかります。

gibasa::posDebugRcpp("月ノ美兎は箱の中") |>
  dplyr::filter(is_best == "01")
#>    doc_id pos_id surface                                 feature stat lcAttr
#> 1       1      0                         BOS/EOS,*,*,*,*,*,*,*,*   02      0
#> 2       1     38      月          名詞,一般,*,*,*,*,月,ツキ,ツキ   00   1285
#> 3       1      4      ノ              記号,一般,*,*,*,*,ノ,ノ,ノ   00      5
#> 4       1     44      美  名詞,固有名詞,人名,名,*,*,美,ヨシ,ヨシ   00   1291
#> 5       1     38      兎      名詞,一般,*,*,*,*,兎,ウサギ,ウサギ   00   1285
#> 6       1     16      は            助詞,係助詞,*,*,*,*,は,ハ,ワ   00    261
#> 7       1     38      箱          名詞,一般,*,*,*,*,箱,ハコ,ハコ   00   1285
#> 8       1     24      の            助詞,連体化,*,*,*,*,の,ノ,ノ   00    368
#> 9       1     66      中 名詞,非自立,副詞可能,*,*,*,中,ナカ,ナカ   00   1313
#> 10      1      0                         BOS/EOS,*,*,*,*,*,*,*,*   03      0
#>    rcAttr     alpha      beta is_best prob wcost  cost
#> 1       0      0.00 -22144.50      01    0     0     0
#> 2    1285  -6190.50 -15954.00      01    1  8537  8254
#> 3       5  -8874.75 -13269.75      01    1  4929 11833
#> 4    1291 -13974.00  -8170.50      01    1  7885 18632
#> 5    1285 -18080.25  -4064.25      01    1  5290 24107
#> 6     261 -18095.25  -4049.25      01    1  3865 24127
#> 7    1285 -22729.50    585.00      01    1  6142 30306
#> 8     368 -23010.00    865.50      01    1  4816 30680
#> 9    1313 -24007.50   1863.00      01    1  6528 32010
#> 10      0 -22144.50      0.00      01    1     0 29526

このような語については、こちらのビネットで説明しているように、制約付き解析を使って次のように強制的に抽出することもできます。

gibasa::posDebugRcpp("月ノ\t*\n美兎\t*\nは箱の中", partial = TRUE)
#>   doc_id pos_id surface                                 feature stat lcAttr
#> 1      1      0                         BOS/EOS,*,*,*,*,*,*,*,*   02      0
#> 2      1     38    月ノ                     名詞,一般,*,*,*,*,*   01   1285
#> 3      1     38    美兎                     名詞,一般,*,*,*,*,*   01   1285
#> 4      1     16      は            助詞,係助詞,*,*,*,*,は,ハ,ワ   00    261
#> 5      1     38      箱          名詞,一般,*,*,*,*,箱,ハコ,ハコ   00   1285
#> 6      1     24      の            助詞,連体化,*,*,*,*,の,ノ,ノ   00    368
#> 7      1     66      中 名詞,非自立,副詞可能,*,*,*,中,ナカ,ナカ   00   1313
#> 8      1      0                         BOS/EOS,*,*,*,*,*,*,*,*   03      0
#>   rcAttr     alpha      beta is_best prob wcost  cost
#> 1      0      0.00 -19563.75      01    0     0     0
#> 2   1285  -6883.50 -12680.25      01    1  9461  9178
#> 3   1285 -15499.50  -4064.25      01    1 11426 20666
#> 4    261 -15514.50  -4049.25      01    1  3865 20686
#> 5   1285 -20148.75    585.00      01    1  6142 26865
#> 6    368 -20429.25    865.50      01    1  4816 27239
#> 7   1313 -21426.75   1863.00      01    1  6528 28569
#> 8      0 -19563.75      0.00      01    1     0 26085

一方で、IPA辞書には、たとえば「早見」のような名詞,固有名詞,人名,姓,...という品詞と、「沙織」のような名詞,固有名詞,人名,名,...という品詞があります。 このような解析結果としてより望ましい品詞を確認するには、正しく解析させたい語(ここでは「月ノ美兎」)と同じような使われ方をする語(たとえば「早見沙織」)を実際に解析してみて、その結果を確認するとよいでしょう。

gibasa::posDebugRcpp("早見沙織のラジオ番組") |>
  dplyr::filter(is_best == "01")
#>   doc_id pos_id surface                                      feature stat
#> 1      1      0                              BOS/EOS,*,*,*,*,*,*,*,*   02
#> 2      1     43    早見 名詞,固有名詞,人名,姓,*,*,早見,ハヤミ,ハヤミ   00
#> 3      1     44    沙織 名詞,固有名詞,人名,名,*,*,沙織,サオリ,サオリ   00
#> 4      1     24      の                 助詞,連体化,*,*,*,*,の,ノ,ノ   00
#> 5      1     38  ラジオ       名詞,一般,*,*,*,*,ラジオ,ラジオ,ラジオ   00
#> 6      1     38    番組     名詞,一般,*,*,*,*,番組,バングミ,バングミ   00
#> 7      1      0                              BOS/EOS,*,*,*,*,*,*,*,*   03
#>   lcAttr rcAttr     alpha      beta is_best prob wcost  cost
#> 1      0      0      0.00 -10104.75      01    0     0     0
#> 2   1290   1290  -4362.75  -5742.00      01    1  7472  5817
#> 3   1291   1291  -5452.50  -4652.25      01    1  8462  7270
#> 4    368    368  -6658.50  -3446.25      01    1  4816  8878
#> 5   1285   1285  -7886.25  -2218.50      01    1  3942 10515
#> 6   1285   1285 -10534.50    429.75      01    1  3469 14046
#> 7      0      0 -10104.75      0.00      01    1     0 13473

この結果は狙いどおりのものであるため、「月ノ美兎」を正しく解析するために用意するCSVファイルは、仮に次のように作成しておくことができそうです。

writeLines(
  c(
    "月ノ,1290,1290,7472,名詞,固有名詞,人名,姓,*,*,月ノ,ツキノ,ツキノ",
    "美兎,1291,1291,8462,名詞,固有名詞,人名,名,*,*,美兎,ミト,ミト"
  ),
  con = (csv_file <- tempfile(fileext = ".csv"))
)

A.2.2 ユーザー辞書のビルド

試しに、ユーザー辞書をビルドしてみましょう。gibasaでユーザー辞書をビルドするには、gibasa::build_user_dicを使います。 ユーザー辞書をビルドするにはシステム辞書が必要なため、あらかじめシステム辞書(ここではIPA辞書)が適切に配置されていることを確認しておいてください。

次のようにしてユーザー辞書をビルドできます。

gibasa::build_user_dic(
  dic_dir = stringr::str_sub(gibasa::dictionary_info()$file_path, end = -8),
  file = (user_dic <- tempfile(fileext = ".dic")),
  csv_file = csv_file,
  encoding = "utf8"
)
#> reading /tmp/RtmpMnG64K/file3812779ae1e4.csv ... 2
#> 
#> done!

なお、gibasaによる辞書のビルド時の注意点として、「MeCab: 単語の追加方法」で案内されている「コストの自動推定機能」はgibasaからは利用できません。 追加したい見出し語の生起コストは空にせず、必ず適当な値で埋めるようにしてください。

さて、ビルドしたユーザー辞書を使ってみましょう。

gibasa::dictionary_info(user_dic = user_dic)
#>                              file_path charset lsize rsize   size type version
#> 1    /var/lib/mecab/dic/debian/sys.dic   UTF-8  1316  1316 392127    0     102
#> 2 /tmp/RtmpMnG64K/file381272a02343.dic    utf8  1316  1316      2    1     102
gibasa::tokenize("月ノ美兎は箱の中", user_dic = user_dic)
#> # A tibble: 6 × 5
#>   doc_id sentence_id token_id token feature                                     
#>   <fct>        <int>    <int> <chr> <chr>                                       
#> 1 1                1        1 月ノ  名詞,固有名詞,人名,姓,*,*,月ノ,ツキノ,ツキノ
#> 2 1                1        2 美兎  名詞,固有名詞,人名,名,*,*,美兎,ミト,ミト    
#> 3 1                1        3 は    助詞,係助詞,*,*,*,*,は,ハ,ワ                
#> 4 1                1        4 箱    名詞,一般,*,*,*,*,箱,ハコ,ハコ              
#> 5 1                1        5 の    助詞,連体化,*,*,*,*,の,ノ,ノ                
#> 6 1                1        6 中    名詞,非自立,副詞可能,*,*,*,中,ナカ,ナカ

狙いどおりに解析できているようです。

A.2.3 生起コストを調整する

ここまでに紹介したようなやり方で辞書を整備することで、おおむね狙いどおりの解析結果を得られるようになると思われますが、 追加した語のかたちによっては、生起コストをより小さな値に調整しないと、一部の文において正しく切り出されない場合があるかもしれません。

たとえば、こちらの記事で紹介されているように、 仮に高等学校,1285,1285,5078,名詞,一般,*,*,*,*,高等学校,コウトウガッコウ,コートーガッコーという見出し語を追加したとしても、 与える文によっては高等学校が狙いどおりに切り出されません。

writeLines(
  c(
    "高等学校,1285,1285,5078,名詞,一般,*,*,*,*,高等学校,コウトウガッコウ,コートーガッコー"
  ),
  con = (csv_file <- tempfile(fileext = ".csv"))
)
gibasa::build_user_dic(
  dic_dir = stringr::str_sub(gibasa::dictionary_info()$file_path, end = -8),
  file = (user_dic <- tempfile(fileext = ".dic")),
  csv_file = csv_file,
  encoding = "utf8"
)
#> reading /tmp/RtmpMnG64K/file3812775f2351.csv ... 1
#> 
#> done!
gibasa::tokenize(
  c(
    "九州高等学校ゴルフ選手権",
    "地元の高等学校に進学した",
    "帝京高等学校のエースとして活躍",
    "開成高等学校117人が現役合格",
    "マンガを高等学校の授業で使う"
  ),
  user_dic = user_dic
) |>
  gibasa::pack()
#> # A tibble: 5 × 2
#>   doc_id text                                
#>   <fct>  <chr>                               
#> 1 1      九州 高等 学校 ゴルフ 選手権        
#> 2 2      地元 の 高等 学校 に 進学 し た     
#> 3 3      帝京 高等 学校 の エース として 活躍
#> 4 4      開成 高等 学校 117 人 が 現役 合格  
#> 5 5      マンガ を 高等 学校 の 授業 で 使う

この例のように、複数の既存の見出し語のほうが優先されてしまう場合には、追加する見出し語の生起コストを小さくすることによって、 狙いどおりの解析結果を得ることができます。

先ほどの記事のなかで紹介されているのと同じやり方で、適切な生起コストをgibasaを使って求めるには、たとえば次のような関数を用意します(やや複雑なのでバグがあるかもしれません)。

calc_adjusted_cost <- \(sentences, target_word, sys_dic = "", user_dic = "") {
  sentences_mod <-
    stringi::stri_replace_all_regex(
      sentences,
      pattern = paste0("(?<target>(", target_word, "))"),
      replacement = "\n${target}\t*\n",
      vectorize_all = FALSE
    )
  calc_cumcost <- \(x) {
    ret <-
      gibasa::posDebugRcpp(x, sys_dic = sys_dic, user_dic = user_dic, partial = TRUE) |>
      dplyr::mutate(
        lcAttr = dplyr::lead(lcAttr, default = 0),
        cost = purrr::map2_dbl(
          rcAttr, lcAttr,
          ~ gibasa::get_transition_cost(.x, .y, sys_dic = sys_dic, user_dic = user_dic)
        ),
        wcost = cumsum(wcost),
        cost = cumsum(cost),
        ## 1行目のBOS/EOS->BOS/EOS間の連接コストを足しすぎてしまうので、引く
        total_cost = wcost + cost - gibasa::get_transition_cost(0, 0, sys_dic = sys_dic, user_dic = user_dic),
        .by = doc_id
      ) |>
      dplyr::slice_tail(n = 1, by = doc_id) |>
      dplyr::pull("total_cost")
    ret
  }
  cost1 <- calc_cumcost(sentences)
  cost2 <- calc_cumcost(sentences_mod)

  gibasa::posDebugRcpp(sentences_mod, sys_dic = sys_dic, user_dic = user_dic, partial = TRUE) |>
    dplyr::filter(surface %in% target_word) |>
    dplyr::reframe(
      stat = stat,
      surface = surface,
      pos_id = pos_id,
      feature = feature,
      lcAttr = lcAttr,
      rcAttr = rcAttr,
      current_cost = wcost,
      adjusted_cost = wcost + (cost1[doc_id] - cost2[doc_id] - 1)
    ) |>
    dplyr::slice_min(adjusted_cost, n = 1, by = surface)
}

この関数を使って、実際に適切な生起コストを計算してみます。

adjusted_cost <-
  calc_adjusted_cost(
    c(
      "九州高等学校ゴルフ選手権",
      "地元の高等学校に進学した",
      "帝京高等学校のエースとして活躍",
      "開成高等学校117人が現役合格",
      "マンガを高等学校の授業で使う"
    ),
    target_word = "高等学校",
    user_dic = user_dic
  )

この生起コストを使って改めてユーザー辞書をビルドし、結果を確認してみましょう。

adjusted_cost |>
  tidyr::unite(
    csv_body,
    surface, lcAttr, rcAttr, adjusted_cost, feature,
    sep = ","
  ) |>
  dplyr::pull("csv_body") |>
  writeLines(con = (csv_file <- tempfile(fileext = ".csv")))

gibasa::build_user_dic(
  dic_dir = stringr::str_sub(gibasa::dictionary_info()$file_path, end = -8),
  file = (user_dic <- tempfile(fileext = ".dic")),
  csv_file = csv_file,
  encoding = "utf8"
)
#> reading /tmp/RtmpMnG64K/file38122ef5d94a.csv ... 1
#> 
#> done!

gibasa::tokenize(
  c(
    "九州高等学校ゴルフ選手権",
    "地元の高等学校に進学した",
    "帝京高等学校のエースとして活躍",
    "開成高等学校117人が現役合格",
    "マンガを高等学校の授業で使う"
  ),
  user_dic = user_dic
) |>
  gibasa::pack()
#> # A tibble: 5 × 2
#>   doc_id text                               
#>   <fct>  <chr>                              
#> 1 1      九州 高等学校 ゴルフ 選手権        
#> 2 2      地元 の 高等学校 に 進学 し た     
#> 3 3      帝京 高等学校 の エース として 活躍
#> 4 4      開成 高等学校 117 人 が 現役 合格  
#> 5 5      マンガ を 高等学校 の 授業 で 使う

今度はうまくいっていそうです。

A.2.4 システム辞書のビルド

ユーザー辞書ではなく、システム辞書をビルドすることもできます。 ただ、ふつうに入手できるIPA辞書のソースの文字コードはEUC-JPであり、UTF-8のCSVファイルと混在させることができないため、扱いに注意が必要です。

また、UniDicの2.3.xについては、同梱されているファイルに問題があるようで、そのままではビルドできないという話があるようです(参考)。 UniDicはIPA辞書に比べるとビルドするのにそれなりにメモリが必要なことからも、とくに事情がないかぎりはビルド済みのバイナリ辞書をダウンロードしてきて使ったほうがよいでしょう。

ipadic_temp <- tempfile(fileext = ".tar.gz")
download.file("https://github.com/shogo82148/mecab/releases/download/v0.996.9/mecab-ipadic-2.7.0-20070801.tar.gz", destfile = ipadic_temp)
untar(ipadic_temp, exdir = tempdir())

gibasa::build_sys_dic(
  dic_dir = file.path(tempdir(), "mecab-ipadic-2.7.0-20070801"),
  out_dir = tempdir(),
  encoding = "euc-jp"
)
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/unk.def ... 40
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Adverb.csv ... 3032
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Conjunction.csv ... 171
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Suffix.csv ... 1393
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.adverbal.csv ... 795
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.others.csv ... 151
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.org.csv ... 16668
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Verb.csv ... 130750
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.place.csv ... 72999
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.csv ... 60477
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Adnominal.csv ... 135
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.number.csv ... 42
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.verbal.csv ... 12146
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Filler.csv ... 19
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Others.csv ... 2
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.adjv.csv ... 3328
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Interjection.csv ... 252
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Postp-col.csv ... 91
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.nai.csv ... 42
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Prefix.csv ... 221
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.name.csv ... 34202
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Symbol.csv ... 208
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Adj.csv ... 27210
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.demonst.csv ... 120
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Noun.proper.csv ... 27327
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Postp.csv ... 146
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/Auxil.csv ... 199
#> reading /tmp/RtmpMnG64K/mecab-ipadic-2.7.0-20070801/matrix.def ... 1316x1316
#> 
#> done!

# `dicrc`ファイルをビルドした辞書のあるディレクトリにコピーする
file.copy(file.path(tempdir(), "mecab-ipadic-2.7.0-20070801/dicrc"), tempdir())
#> [1] TRUE

# ここでは`mecabrc`ファイルが適切な位置に配置されていないという想定で、
# `mecabrc`ファイルを偽装している。
withr::with_envvar(
  c(
    "MECABRC" = if (.Platform$OS.type == "windows") {
      "nul"
    } else {
      "/dev/null"
    }
  ),
  gibasa::tokenize("月ノ美兎は箱の中", sys_dic = tempdir())
)
#> # A tibble: 8 × 5
#>   doc_id sentence_id token_id token feature                                
#>   <fct>        <int>    <int> <chr> <chr>                                  
#> 1 1                1        1 月    名詞,一般,*,*,*,*,月,ツキ,ツキ         
#> 2 1                1        2 ノ    記号,一般,*,*,*,*,ノ,ノ,ノ             
#> 3 1                1        3 美    名詞,固有名詞,人名,名,*,*,美,ヨシ,ヨシ 
#> 4 1                1        4 兎    名詞,一般,*,*,*,*,兎,ウサギ,ウサギ     
#> 5 1                1        5 は    助詞,係助詞,*,*,*,*,は,ハ,ワ           
#> 6 1                1        6 箱    名詞,一般,*,*,*,*,箱,ハコ,ハコ         
#> 7 1                1        7 の    助詞,連体化,*,*,*,*,の,ノ,ノ           
#> 8 1                1        8 中    名詞,非自立,副詞可能,*,*,*,中,ナカ,ナカ