論理の流刑地

地獄の底を、爆笑しながら闊歩する

{stm}パッケージ用に、日本語ドキュメントのベクトル⇒BoW表現のリスト&単語ベクトル、の変換をする

別記事を書いていて、トピック単体で切り出したほうがいいなと思ったので。
その記事はたぶん近日あげる*1

Introduction

{stm}パッケージ(構造的トピックモデルを推定するためのRパッケージ, CRANのURL)を使うための自分用備忘の記事を、vignetteとか読みつつ書いていた。

で、当たり前だけど元々は英語圏のパッケージなので、基本的に日本語特有の処理のことはvignetteその他には書いていない。
あと私は普段自然言語処理とか関係ない人生を送っているので、そこらへんの土地勘がない。
なので、調べながら備忘メモを残す。

解くべき問題

stmのメイン関数stm()を推定するとき、第一引数documentsと、第二引数vocabを指定する必要がある

documentsは、文書数だけ要素数があるリストで、各要素は

  • 一行目に単語の種類を表すindex
  • 二行目に単語の登場回数

を格納した2×(登場した単語の種類)の大きさの行列である(下の画像参照)

で、vocabはこの一行目の各indexと対応するのが何かを表す単語である

これを、日本語テキストが対象のときにどうするか、という話。

Data

大昔につくった、2020年J1リーグの監督の試合後会見コメントのデータを使う。取得時点での終了試合なので正確には2020年シーズンのうち、2020/10/14までの会見データである

監督試合後会見コーパス

How to transform

色々調べながら変換にいたるまでの悪戦苦闘を記録しておく

RMeCabでターム-文書行列を得る

徳島大石田先生のRMeCab(URL)をつかってみる

docDF()関数を使うと、ターム-文書行列が得られる(引数type=1にする必要がある*2
以下では、

  • minFreq = 5を使って、トータル5回以上出ている語だけに絞る
  • posで、名詞・動詞・形容詞だけ
retTerm2 <- docDF(dfCmnt20, "Comment" , type = 1,
                  pos = c("形容詞","動詞","名詞"), minFreq = 5)

tail( retTerm2,3)


出力は↑のように、行が各単語、列が単語/品詞/i番目の文書における登場回数..という形のデータフレームになっている。
なんとなく、うまく変形すれば求める形になりそうな予感だ。

BoW表現を得る

stm::stm()にかけられるような表現をここから得るような関数を以下のようにかく
処理内容としては、

  • docDF()の戻り値にIndexを付与して、各RowXX列(文書を表す)が0でない=その単語が登場してない行を抜いて行列化→置換して、stm()の第一引数にかけられるようなリストにしてる
  • TERMから単語ベクトルを取得して、stm()のvocab引数に渡すベクトルとする
  • 一応品詞情報もデータフレームとして取得しておく($dfVocab)

をやってる

docDF_toBoW <- function( doc_df ){
  ## args;
  # doc_df:  RMeCab::docDF( .. , type = 1, ...)の戻り値
    doc_df <- doc_df %>% mutate( Index = row_number()) #
  
  # 語彙ベクトル(と品詞情報)を得る
  dfVocab <- doc_df %>% select( TERM , POS1 , POS2 , Index)
  vecVocab <- dfVocab$TERM
  
  # stm::stm()の引数documentsに指定できるような形に変換する
  doc_n <- doc_df %>% select(starts_with( "Row")) %>% ncol #文書数の取得
  doc_info <- list() #空のリストとして初期化
  
  for( i in 1:doc_n){
    # cat( i, ",")
    row_tgt <- paste0( "Row", i)
    row_q <- rlang::parse_expr(row_tgt)
    
    tgt_df <- doc_df[, c("Index",row_tgt )]
    term_cnt <- tgt_df %>% filter( !!(row_q) > 0 )
    
    doc_bow <- term_cnt %>% as.matrix() %>% t()
    
    doc_info[[i]] <- doc_bow
    
  } #for 
  
  return( list(BoW = doc_info,
               Vocab = vecVocab,
               dfVocab = dfVocab
              ))
  
} #function

#使用例:形容詞・動詞・名詞かつ登場回数が5回以上に品詞を限定
retTerm2 <- docDF(dfCmnt20, "Comment" , type = 1,
                  pos = c("形容詞","動詞","名詞"), minFreq = 5)
retBoW2 <- docDF_toBoW(retTerm2)

# ver2: さらに種類を絞る
jclb20 <- c( "東京","札幌","川崎","大阪","名古屋","鹿島", "柏","横浜","大分","浦和","鳥栖","神戸","清水","仙台","湘南")
retTerm2_v2 <- retTerm2 %>% filter( !(POS2 %in% c("数", "代名詞","接尾","非自立")) ) %>%
  filter( !(TERM %in% jclb20 )) 
retBoW2_v2 <- docDF_toBoW(retTerm2_v2)

監督コーパスに対して適用してみた結果は以下の通り

それっぽい形に頻度行列を要素としたリストができてる。


サッカーっぽい単語ベクトルができてる。

おまけ:ちゃんとstm::stm()が適用できるかのチェック

推定まで

お前ほんとにそれ{stm}の前準備になってんのかよ!!というお叱りもあるかもしれない(ごもっともだ)
ので、実際にやってみる。

{stm}は文書属性情報を共変量として用いることができるのがメリットなので、

  • 得点数
  • 失点数
  • 得点期待値(Football LAB算出)
  • ボール支配率

を紐づけてみよう

j1_meta20 <-  left_join(dfCmnt20 ,
                        j1_stat %>% select( Date , Club ,
                    Goal_For , Goal_Against , Expected_Goal,Possession = Possesion),
                    by = c("Club","Date") )

登場回数が多すぎる単語と、少なすぎる単語を除外して前準備する


prep_JBoW20_v2 <- prepDocuments( retBoW2_v2$BoW , vocab = retBoW2_v2$Vocab,
                              meta = j1_meta20 , lower.thresh = 10 , 
                              upper.thresh = 350)


トピック数を10に設定し(10特に意味のない適当な数字。本当はsearchK()関数などを使って、精査したほうがよい)、STMを走らせる。

jSTM_k10<- stm::stm( documents = prep_JBoW20_v2$documents,
                     K = 10,
                     data = prep_JBoW20_v2$meta , 
                     vocab = prep_JBoW20_v2$vocab ,
                     prevalence = ~ Goal_For + Goal_Against + Expected_Goal + Possession)

推定後の確認

推定された各トピックの代表的wordは、以下のようになっている

jSTM_k10 %>%  plot.STM( type="labels")
2020年J1のトピックの代表的単語

代表的なトピックについて観察していくと、

  • 2020年はコロナ禍最初のシーズンで、数か月の中断を挟んだこともあり、かなり詰め込まれた修正日程が組まれ、かなりハードスケジュールであった。「連戦」というワードを含むTopic6/8はそういったピッチ外の環境に言及したものである
    • 「サポーター」「感謝」「思い」などが入っていることもあり、Topic6のほうがより周りへのメッセージという意味合いが強く、Topic8は「コンディション」「チーム」といった単語を含み、よりチームへの影響に重点が置かれている
  • Topic9は、相手の最終ラインの枚数への言及(「バック」)や、「質」「市」「前線」といったワードを含むことから、戦術的な振り返りを行うトピックである
    • stm::findThougts()は各ドキュメントに対して算出される、トピックの割当事後確率をもとに、各トピックと関連の高い文書を上位から取り出せる関数である(詳しくは下のコード参照)。たとえばTopic9の割合が高い会見コメントとしてtop3に入るものとして、下平監督(当時横浜FC)の以下のようなコメントがあげられる

前半の最初は本当に良い入りをして、相手も面を食らったような感じで自分たちのペースでやりたいことができていた。ただ、時間帯によっては疲れてきて、やりたいことができない時間もある。守備は確かにマンツーマンでやったけど、そこはリスクを負ってやったので、そこで良い形で奪えれば自分たちに点が転がってくるし、入れ替えられれば相手のビッグチャンスになる。そういうリスクを負ってやっているので、球際やフィニッシュのところを含めて勝ち切れるようになっていったら、もっと面白いかなと思います。失点の形は前節もそうですけど、クロスからやられている。3バックでやっているぶん、5枚にハッキリなっていれば完全にはね返せるけど、3バックに任せることによって、3バックの大外でいつもやられていることが多い。ウイングバックをやる選手の守備力はもっと上げないといけない。かなりハードなタスクを攻守で課しているので、そこで戻り切れなくなっているという戦術的な理由も当然あると思うけど、そこは逆に言えば、3バックの選手にははね返せる力をつけてほしいし、選手の能力をもっと伸ばしていきたい。ハイラインが特徴的なチームなので、そこの背後はFC東京と鹿島のゲームを参考にだいぶ狙っていました。ただ、守備力のところで鹿島やFC東京ボランチのところでボールを取れて、そこからのカウンターが効果的にできていた。なかなかウチは中盤でボールを奪えなかった。プランはあったにせよ、それを遂行する力が足りなかったと思いました

ーー2020年7月, 横浜ダービー後の下平監督コメント(URL

↓代表的文書をとりだすためのRコード

# topic 9のhighly associated documentsを上位4個とりだす
fT9 <- findThoughts( jSTM_k10 , texts = dfCmnt20$Comment , n = 4 , topics = 9) #トピック9を4個
fT9$docs[[1]]


ということで、
日本語ドキュメントからBoW形式変換→前処理して、{stm}パッケージで分析するさいの段取りが分かった


www.youtube.com

Enjoy!!

*1:と言ってあげないことがままある自分だということも、私は知っている。私が一番信用ならないのは私だ

*2:type=0だとNgram行列がかえってくる