論文メモ: Latent Aspect Rating Analysis on Review Text Data: A Rating Regression Approach
Latent Aspect Rating Analysis on Review Text Data: A Rating Regression Approach (KDD'10) という論文について紹介します。
@inproceedings{Wang:2010:LAR:1835804.1835903, author = {Wang, Hongning and Lu, Yue and Zhai, Chengxiang}, title = {Latent Aspect Rating Analysis on Review Text Data: A Rating Regression Approach}, booktitle = {Proceedings of the 16th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining}, series = {KDD '10}, year = {2010}, isbn = {978-1-4503-0055-1}, location = {Washington, DC, USA}, pages = {783--792}, numpages = {10}, url = {http://doi.acm.org/10.1145/1835804.1835903}, doi = {10.1145/1835804.1835903}, acmid = {1835903}, publisher = {ACM}, address = {New York, NY, USA}, keywords = {algorithms, experimentation}, }
この論文の貢献
1つのレビューの文章から観点ごとについてポジティブ/ネガティブに評価されているのか知る、Latent Aspect Rating Analysis (LARA)というテキストマイニングの問題を新たに定義する。
そしてこのLARAを解く手法として、観点ごとの評価値を予測する回帰モデル Latent Rating Regression Model (LRR) を提案する。
Latent Aspect Rating Analysis (LARA)
レビューの文章では、複数の観点 (aspect) の評価が混在して書かれているのが普通です。たとえば以下のようなホテルのレビュー (原文Figure 1より抜粋) で見てみると、場所のこと・部屋のこと・サービスのこと・金額のことなど、それぞれ記述されています。
一方で数値化された評価は、扱う商品ごとに1つ (Overall Rating) となっているケースが多いです。レビュアーの手間を省いてレビューを増やすことの方が重要であったり、あるいは、総合ECサイトのように扱う商品が膨大で統一的な観点を設けられない (カメラと電動ノコギリで知りたい観点は違うはずです) など、1つの評価値のみにせざる得ない場合も多いかと思います。
しかし、観点ごとの評価を知りたい、というニーズもあります。ユーザによって重視する観点が異なるので、ユーザとしては各観点のサマリが欲しかったり、あるいは、ユーザが重視する観点を発見して評価が高い商品をおすすめしたりなどです。
上のニーズを汲み取った問題設定として、LARAでは新たに各観点の評価値と重みをレビューごとに推定します。
インプット
上の図のような文章 + 総合評価値のレビューが対象となります。
- レビュー文
- 文書に現れる単語
- 文書に現れる単語
- 総合評価値 (overall rating)
- 観点の数
- 観点を特徴づけるいくつかのキーワード
- 例えば、観点Priceを特徴づける単語は"price", "value", "worth"など
アウトプット
- 観点の評価値
- k次元のベクトルで、観点iの値は
となります
- k次元のベクトルで、観点iの値は
- 観点の重み
- これもk次元のベクトルで、観点iの重みは
となります
- 1に近いほうが、そのレビューにとって重要な評価値
- これもk次元のベクトルで、観点iの重みは
Latent Rating Regression Model (LRR)
LRRでは2つのステップで観点ごとの評価値と重みを導出します。
- 全レビュー文書から、観点ごとに文を分ける
- LRRモデルを仮定して、パラメータの推定と評価値・重みを計算を行う
観点ごとに文を分ける
最初にどの文がどの観点について述べているのかを特定する必要があります。
観点を特徴づけるキーワードとして の初期値をハイパパラメータとしてモデルを作る人が定義します。ホテルの場合、7つの観点を設定したとすると、
の初期値は以下のようになります (原文Table 1より抜粋) 。
この を、次の手順で更新しながら、最終的に各文が述べている観点を特定します。カイ2乗検定 (独立性の検定) で、ある観点を述べる文と単語の間で関連が高いものは、その観点を述べているものとしてTに追加しています。
- 全てのレビューを文単位で分ける
- 以下をI回繰り返す
- Xの各文で、
に属する単語の出現回数をカウントする
- Xの各文で、最も出現回数の多い
と対応する観点
を、その文が述べている観点として割り当てる
- 各単語のχ2乗値を計算する
- χ2乗値が最も大きいトップp個の単語を選び、
に追加する
- Xの各文で、
最終的に、レビューdごとに行列 を得ます。この行列 (k×n) の各要素
は観点
に割り当てられた単語
の出現回数を、dの単語数で割って正規化された値 (つまり出現確率) が対応している。
LRRモデル
次にLRRモデルで各観点の評価値 と 重み
を推定していきます。
LRRモデルでは、3段階のプロセスを経てレビューされるという前提に基づきます。
- レビューする観点を決める
- 各観点について単語を選んで文にする
- 各観点の重要度を含めて総合評価をつける
したがって、文の単語が総合評価を直接反映するのではなく、文の単語→各観点の評価→総合評価というモデルになります (原文Figure 3より抜粋、記号の意味などは後ほど) 。
観点の評価値は、上で得られた観点ごとの語の出現確率 と各単語の極性
によって決まります (1) 。
- 直感的には、"worth"は観点Valueにポジティブな影響を与えますし、"dirty"は観点Roomsにネガティブに働くはず、というイメージです
- 極性
はパラメータとして推定します
$$ \vec{s_i} = \sum_{j=1}^{n} \beta_{ij}\pmb{W}_{dij} $$
総合評価 はガウス分布に従うと仮定してます (2) 。
- 重み×各観点の評価値の和が、総合評価値の平均
- 分散
も推定対象のパラメータ
$$ r_d \sim N(\mu, \delta^2) = N(\sum_{i=1}^{k}\alpha_{di} \sum_{j=1}^{n}\beta_{ij} \pmb{W}_{dij}, \delta^2) $$
重みもまたガウス分布に従うとしてます (3) 。
- 平均
と分散
は推定対象
$$ \alpha_d \sim N(\mu, \Sigma) $$
パラメータの推定と評価値・重みの計算
(2)と(3)を組み合わせて、ベイズ回帰の問題に落とし込みます。
$$ P(r|d) = P(r_d | \mu, \Sigma, \delta^2, \beta, \pmb(W_d) = \int p(\alpha_d | \mu, \Sigma) p(r_d | \sum_{i=1}^{k} \alpha_{di} \sum_{j=1}^{n} \beta_{dij} \pmb{W}_{dij}, \delta^2) d\alpha_d $$
以上より、推定しないといけないパラメータは となります。これらパラメータは以下のステップで求めます。
- 各レビューdの
を(1)で計算する
- 対数尤度関数
の最尤推定量
を計算して
導出する (MAP推定)
最尤推定量はEMアルゴリズムで求めていきます (詳しくは、原文4.3 LRR Model Estimationを見てください) 。
所感
実験の章はさっと読んだだけなのですが、最後に読んだ所感を挙げておきます。
- 観点の数kとSeed wordsの選定は、ドメイン知識が必要
- 観点の数が多くなりすぎると、観点を表す語がレビューに現れにくくなって、スパースになりやすい (実験でも見られている)
では単語の出現頻度で作っているが、word2vecなどの埋め込みを使えばスパースを解消できるかも...?
小ネタ: urllibでURLをパースする・生成する
urllibを使ったURLのパースと生成についてまとめます。よく使うのに、そのたびに調べてしまっているので。
パースする
まずはURLをパースする方法ですが、urllib.parse.urlparseにURL文字列を渡すだけです。
あとは、返されたParseResultオブジェクトから必要な情報 (ホスト名, パス, クエリパラメータ, フラグメント) を取得します。
from urllib.parse import urlparse u = 'https://ohke.hateblo.jp/archive/category/Python?page=2&q=pandas#1' result = urlparse(u) print(result) # ParseResult(scheme='https', netloc='ohke.hateblo.jp', path='/archive/category/Python', params='', query='page=2&q=pandas', fragment='1') print(result.hostname, result.path, result.query, result.fragment) # ohke.hateblo.jp /archive/category/Python page=2&q=pandas 1
デコードする
URLエンコードされていてそれをパース前にデコードしたい場合は、urllib.parse.unquoteでデコードしてしまいます。
from urllib.parse import unquote u = 'https://ohke.hateblo.jp/archive/category/%e3%82%af%e3%83%ad%e3%83%bc%e3%83%a9?page=2&q=%e3%81%91%e6%97%a5%e8%a8%98#1' decoded = unquote(u) print(decoded) # https://ohke.hateblo.jp/archive/category/クローラ?page=2&q=け日記#1
クエリパラメータを辞書にする
クエリパラメータはurllib.parse.parse_qsで辞書に変換できるので、さらに扱いやすくできます。
from urllib.parse import parse_qs query_dict = parse_qs(result.query) print(query_dict) # {'page': ['2'], 'q': ['pandas']}
生成する
URLを生成する場合は、urllib.parse.urlparseに各パーツを渡せばOKです。
from urllib.parse import urlunparse u = urlunparse(( 'https', 'ohke.hateblo.jp', '/archive/category/Python', None, 'page=2&q=pandas', '1' )) print(u) # https://ohke.hateblo.jp/archive/category/Python?page=2&q=pandas#1
小ネタ: Pandasでqueryを使って行を選択する
PandasのDataFrameから行を抽出する簡便な方法として、queryメソッドが提供されています。
SQLで言えば選択 (WHERE句) にあたる処理を、文字列で記述できます。
queryメソッドを使った選択の例
今回の投稿で使うデータを準備します。
import pandas as pd df = pd.DataFrame({ 'name': ['荒岩 一味', '荒岩 虹子', '荒岩 まこと', '荒岩 みゆき', '田中 一', '梅田 よしお'], 'age': [44, 44, 21, 13, 36, 31], 'sex': ['M', 'F', 'M', 'F', 'M', 'M'], })
以下のように第1引数に条件文を記述することで、行を選択できます。完全一致で選択する場合は、列名 == 値
とします。
df.query("name == '荒岩 一味'") # age name sex # 0 44 荒岩 一味 M
条件文内で@変数名
とすると、Pythonの変数の値を参照できます。
search_name = '田中 一' df.query("name == @search_name") # age name sex # 4 36 田中 一 M
複数条件で選択することもできます。&
でAND条件、|
でOR条件となります。ちなみにBETWEEN句などはありません。
df.query("sex == 'M' & age >= 35") # age name sex # 0 44 荒岩 一味 M # 4 36 田中 一 M df.query("age >= 40 | age < 20") # age name sex # 0 44 荒岩 一味 M # 1 44 荒岩 虹子 F # 3 13 荒岩 みゆき F
IN句も以下のようにすることで記述できます。
df.query("name in ('荒岩 まこと', '荒岩 みゆき')") # age name sex # 2 21 荒岩 まこと M # 3 13 荒岩 みゆき F
LIKE検索は少し特殊です。LIKE句はありませんが、以下のようにengine='python'
とすることで、条件文中でSeriesのstrメソッドを利用できます。あとはstartswithメソッドを使うことで、name LIKE '荒岩%'
といった選択もできます。startswith以外にも、endswith, containsなども利用できます。
df.query("name.str.startswith('荒岩')", engine='python') # age name sex # 0 44 荒岩 一味 M # 1 44 荒岩 虹子 F # 2 21 荒岩 まこと M # 3 13 荒岩 みゆき F
queryメソッドの注意点
上のように簡単に行選択を記述できるqueryメソッドですが、Series (bool型) による選択と比較すると、若干遅いようです。速度を気にする場合は、利用を避けたほうが無難そうです。