ここでは、Exploratoryを使って、トピックモデリングをやってみようと思います。
別の機会で、ノートを書くことがあったので、そのノートから実行する部分を抜き出して、誰かの参考になればと思い、パブリッシュしておきます。調べてみた限り、現状、ExploratoryのサイトにLDAのHowto記事がなかったので・・・たぶん。あったらごめんなさい。
トピックモデルは潜在的意味を推定するためのモデルとか言われているやつで、ここではLatent Dirichlet Allocation(LDA)を取り上げます。モデルの説明については取り扱わないので、参考書などを参照ください。
サンプルデータはKaggleのAmazonレビューデータを使用します。
また、レビューデータのProductId
がB007JFMH8M
のレビューに限定します。このクッキーのレビューは913件です。
Amazonのサイトによると、このクッキーの特徴は下記の通りです。
インポートしたデータはこんな感じです。
ここでは、このレビューを文字の処理 >> トークン化 >> 文字の処理 >> 単語のカウント >> 文書単語行列(DTM) >> LDA
という順番で処理します。この手順が絶対に正しいというわけでもないので、参考程度にしていただければと思います。
文書単語行列(DTM) >> LDA
の部分が、Rのトピックモデリングを紹介しているブログなどでは、分割して処理するスクリプトが紹介されているのが一般的かと思います・・・が、Exploratoryでは、各ステップでデータフレームを返す必要があるため(間違ってたら、すいません。)、紹介されているスクリプトをそのまま利用すると、データフレームを返さないので、エラーになってしまいます。
なので、その一連の処理を下記の関数で行い、LDAの結果を得ることにします。
文書単語行列(DTM)の変換
とLDAの計算
は、各処理がデータサイズによっては重たくなるので、試行錯誤をすることを考えると分けたかったのですが、あまり良い方法が思いつきませんでした・・・。
# pacman::p_load(topicmodels, tidytext)
do_lda <- function(data, document, term, value, weighting, k, matrix, seed){
document_enquo <- rlang::enquo(document)
term_enquo <- rlang::enquo(term)
value_enquo <- rlang::enquo(value)
res <- tidytext::cast_sparse(data = data, row = !!document_enquo, column = !!term_enquo, value = !!value_enquo) %>%
tm::as.DocumentTermMatrix(x = ., weighting = weighting) %>%
topicmodels::LDA(x = ., k = k, control = list(seed = seed)) %>%
broom::tidy(., matrix = matrix)
return(res)
}
ここでは、tf-idf
変換などはせずに素直にカウントし、トピック数は2
で「トピックごとの単語の出現確率(beta
)」を計算しています。他にも、gamma
などもあるので、詳しくは下記を参照ください。
トピックごとの単語の出現確率TOP10は下記となりました。例えば、2行目、12行目のsoft
は、トピック1に出現する確率はおよそ5.6%である一方で、トピック2に出現する確率はおよそ3%と解釈できます。このようにトピックに含まれる単語から各トピックを検討することになります。
このままでは見にくいので可視化します。
カテゴリ | 単語 | トピック内容 |
---|---|---|
トピック1 | cookie, soft, oatmeal, Influenster, received, delicious | InfluensterのレビューからAmazonの商品を買う? |
トピック2 | cookie, soft, kid, snack, individually | 子どものお菓子 |
どうでしょうか。トピック1では、クッキーの評価とは関係なさそうなInfluenster, received
がよく出現するようです。Influenster
を最初に見た時に、誤字かと思ったのですが、received
という単語が気になったので、検索してみたところ、レビュープラットフォーム「Influenster」というサービスが実際にアメリカにはあるそうです。
このことからトピック1は「Influensterのレビューがキッカケ(通知をreceived
?)で、Amazonで商品を買った」というようなトピックがあるのかもしれません。
一方でトピック2では、kid, snack, individually
というトピック1には出現しいくい単語が出現しやすいことから「子どものお菓子」というトピックなのかもしれません。
cookie, soft
などは両方のトピックに共通して出現している単語がありますが、これはLDAのハードクラスタリング方式によるもので、トピックに単語の重なりが含まれることから、それを表現しているLDAの長所とも言われるそうです。
他にも、トピック1とトピック2のbeta
の対数比をとって、差が最も大きいもに着目する方法などもあったりあしますが、ちょっと時間がなくなったので、ここまでとします。
間違いがあったら申し訳ないです・・・
# Set libPaths.
.libPaths("/Users/aki/.exploratory/R/3.6")
# Load required packages.
library(janitor)
library(lubridate)
library(hms)
library(tidyr)
library(stringr)
library(readr)
library(forcats)
library(RcppRoll)
library(dplyr)
library(tibble)
library(bit64)
library(exploratory)
# Steps to produce the output
exploratory::read_delim_file("../committed_data/amazon_review.csv" , ",", quote = "\"", skip = 0 , col_names = TRUE , na = c('','NA') , locale=readr::locale(encoding = "UTF-8", decimal_mark = ".", grouping_mark = "," ), trim_ws = TRUE , progress = FALSE) %>%
readr::type_convert() %>%
exploratory::clean_data_frame() %>%
select(-ProductId) %>%
mutate(Text = str_clean(Text), Text = str_replace_all(Text, "<.*?>", "")) %>%
unnest_tokens(word, Text) %>%
mutate(word = str_replace_all(word, "_", ""),
word = str_to_lower(word)) %>%
mutate(word = purrr::map(.x = word, .f = function(x){
SemNetCleaner::singularize(x)
})) %>%
unnest(word) %>%
anti_join(stop_words, by = "word") %>%
filter(word %nin% c("cooky", "loved", "oat", "wa", "quaker")) %>%
count(UserId, word) %>%
do_lda(
data = .,
document = UserId,
term = word,
value = n,
weighting = tm::weightTf,
k = 2,
matrix = "beta",
seed = 1989
) %>%
group_by(topic) %>%
top_n(10, beta) %>%
arrange(topic, desc(beta)) %>%
mutate(beta = round(beta * 100,2)) %>%
ungroup()