レビューのスコア予想問題
背景
- 商品やサービスを論じるときに、その文脈から定量的にどの程度良かったのか、悪かったのか知ることは難しい
- 幸いなことにネットには膨大な商品とサービスのレビュー件数が存在し、サービスごとのドメインが異なってもある程度、定量的に文章から良し悪しを把握することができる
- ある程度、機械学習アルゴリズムにより正しく分離できるのならば、分離したときの素性の重要度等の情報を用いて、文章が星がつきやすい文章なのか、星がつきにくい文章なのか判断する指標として使うことができる
- 仮説ですが、星がつきやすい文章はポジティブであり、星がつきにくい文章はネガティブだと考えることができるように直感的には感じます
図1. ベクトル化した図の例
- 青のプロットを、星の数が少ないレビューとして、オレンジの部分がレビューの数が多いとすると、以下のような分離面が構築できます。図示しやすいように2次元ですが、実際には60000次元と、かなり高次元でスパースなデータになっています。
手法
データ
- データの配布は禁止されているので、配布することができません。申し訳ありません。
- 20161205までに集めた13万件のレビューをもとに、分類を行いました。
- KVSにURL: {stars: 1~ 5, contents: レビュー内容}のように保存することで、データを管理しています。
- 星の分布には偏りがあり、星が5が圧倒的に多く、星1, 2, 3は少なく、1, 2, 3は足し合わせても、星5の総数には及びません。
- そこで、星5と星1,2,3の分離問題として捉えて、分離するようにしました。
図3 星の分布@N11000程度の場合
XGBoostによる分類
- XGBoostはランダムフォレスト等と系譜を同じくするディシジョンツリーの機械学習アルゴリズムです。非常に高い精度で分離できるため、2015年からKaggle等で用いられるようになっているようです。[1]
- eta: 0.3
- gamma: 1.0
- min_child_weight: 1
- max_depth: 20
$ wc -l differ20161205-2.svm 90233 $ head -n 75000 differ20161205-2.svm > train.txt $ tail -n 15000 differ20161205-2.svm > test.txt $ xgboost t.conf [16:57:31] 75000x93504 matrix with 8267281 entries loaded from train.txt [16:57:31] 15000x93498 matrix with 1651122 entries loaded from test.txt …. [17:00:58] src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 144 extra nodes, 300 pruned nodes, max_depth=20 [17:00:58] [99] test-error:0.048600 train-error:0.002213 [17:00:58] update end, 112.189 sec in all
- train errorが4.86%であり、十分、実用に耐えられるレベルで分離できているように見える。
liblinear(ロジスティック回帰)による分類
- - 最もシンプルな分類であると思われます。収束も早く、このロジスティック回帰に対してどの程度の性能が出るかで性能を確認したりすることもあります。
$ liblinear-train -s 0 train.txt .............................................*....................................................... optimization finished, #iter = 1000 WARNING: reaching max number of iterations Using -s 2 may be faster (also see FAQ) Objective value = -30.960220 nSV = 24990 $ liblinear-predict test.txt train.txt.model result Accuracy = 94.12% (14118/15000)
- 94.12%とXGBoostには及ばないけれど、十分に性能が発揮されているように思われます。学習の結果、素性の重要度の解釈が容易なので、ロジスティック回帰の結果のウェイトを確認します。
ロジスティック回帰の重みの結果
- 単語一つにつき、一次元というパワープレイをしているので、単語数がたくさんないと正しく学習できないのですが、900,000件もあれば、多分大丈夫です。
- (注)libsvmとliblinearは、0番目のインデックスは読み込むことができないので、スルーしていますが、本来はこのようなアドホックなことはすべきではありません。
図4 ポジティブ、ネガティブの素性
モデルの定性的評価
- ロジスティック回帰は非常に単純な式で表現できるため、特徴量の重要度が分かれば自分で計算することができます。
図5. アルファ、ベータに特徴量の重要度が入ります
- プログラムにしてもわずか数行です。Python2ですが、好きなのを使ってください
tsv = filter(lambda x:x != '', open('stash/star_ranking.tsv').read().split('\n')) trank = {} for t_w in tsv: t, w = t_w.split(' ') trank[t] = float(w) idf = json.loads(open(filename + '.idf.json').read()) m = MeCab.Tagger ("-Owakati") for line in sys.stdin: score = 0. line = line.strip() for t in m.parse(line).strip().split(' '): if idf.get(t.decode('utf-8')) != None and trank.get(t) != None: score += idf.get(t.decode('utf-8')) * trank.get(t) res = int(1. / (1. + math.pow(math.e, score*-1 ) ) * 100) if res < 50.: print("だめっぽい文章", end=" ") else: print("よいっぽい文章", end=" ") print("score =", res)
定性評価
- 自分で作ったデータセットに対して、予想したとおりの性能を発揮するか確認します。
$ echo "風邪を引いて頭がとても痛いので家で寝ていたいめう。" | python Review.py --mode score --file tmp/differ20161205 だめっぽい文章 score = 44 $ echo "冬のボーナスが出たので新年は新しい家族をむかえてハワイに旅行に行く。" | python Scraper.py --mode score --file tmp/differ20161205 よいっぽい文章 score = 55
- 大丈夫っぽいですね。
結論
- 日本語の意味しているところを正しく分類しようとすると、次元数が多すぎて分類するの大変で性能がでないのではないかと危惧していた時代もありましたが、意外とかんたんにできるので、応用の範囲は広そうだと感じました。
- 単語のtfidfしか見ていないので、文章構造は無視しているので、これを取ろうとすると、また違った仕組みが必要になります。個人的にはRNNなどで分類するのがちかいと思います。LSTMなどの仕組みが長文を正しく認識できるのか、いまいち自信がないのですが…
- これをセンチメント分析の分類器として使えないかどうか調べているのですが、微妙にレビューと使っている単語の分布が異なっており、専用のデータ・セットの必要性を感じています。[2]