AIQ株式会社

fastTextでセンチメント分析

概要

10億語を数分で学習すると言われている、FacebookのfastTextを使ってセンチメント分析をやってみようと思います。

センチメント分析とは

大塚商会のページから引用させてもらうと

テキストマイニング技術などを使い、ソーシャルメディア上の書き込みから、商品や事象に関する消費者の感情を分析する手法。
「先週発売した新商品が、消費者にどう受け止められているか知りたい」。例えばメーカーの商品開発担当者がこんなニーズを持ったとき、従来は市場調査や購入者を対象にしたアンケート調査などを行う必要があった。
ブログやSNS(ソーシャル・ネットワーキング・サービス)の普及で、こうした調査を簡易にできるようになった。商品名をキーワードに検索し、どれくらい話題になっているかを把握するソーシャル・リスニングを行うのだ。

教師データの収集

Amazonの商品レビューの投稿コメントと1〜5までの星データを教師データとして利用してみます。
データの取得方法ですがAmazon Product Advertising APIとスクレイピングを利用して取得しました。
(コードの公開は控えておきます。)

最終的に、22,732件のデータを収集しました。
1%を検証データとして利用し、残りを教師データに利用します。

5.0, 味の違いはさほど分かりませんが一番搾りはおいしいですね。少しお得に買えたので満足です。
5.0, 母の日と父の日を兼ねて実家に送りましたとても喜んでくれていい買い物をした気分でした
4.0, 色んな地域のビールが、入っていて、味も地域ごとに違う事に驚きながら、美味しく頂きました。
4.0, 1本1本違う種類のラベルと味で楽しめました。また購入したいです
4.0, 話題のキリン一番搾り、近くの酒屋さんで見たよりも、Amazonでは安い価格で出てたので、すぐに注文。娘からの母の日のプレゼントに貰いました。楽しみに飲んでます♪
4.0, 同じビールでも僅かに変わる旨味を味わえるというのは、詰め合わせならでは。あわよくば少し安ければとも思いますが、まぁ美味しかったので☆4つで。

Tableauでデータの確認

とりあえず取得したデータを可視化してみます。
pandasやExcelでも良いのですが、今回は人気のBIツール「Tableau」で視覚化してみます。

Ratingごとにどれくらいのデータが取得されているかグラフ化してみました。

  • csvを読み込み
  • ateをディメンション化
  • 列にrate、行にレコード数

という設定にすると確認できます。

Tableau

星5のコメントが1番多く、星2のコメントが1番少ないのが分かります。
恐らく、星が少ない商品はストアから削除されていくため、星3以上のデータが多くなってしまうようです。

教師データとしてはあまり宜しくない感じはしますが、とりあえずfastTextに入力してみます。

fastTextでのクラス分け

fastTextのインストールはpipで行えます。

pip install fasttext

fastTextでクラス分けをする際の教師データは、下記のようなフォーマットなのでcsvから変換します。

__label__{ラベル番号}, {日本語のわかち書き}

変換時に、Stop Wordの処理をしたりしますが、まずはStop Word無しで学習させてみます。

# -*- coding: utf-8 -*-
# to_fasttext.py
import csv
import re
import sys

import MeCab


def convert2fasttext(rate):
    """ fastText用のラベルに変換 """
    labels = {
        "1.0": "__label__1",
        "2.0": "__label__2",
        "3.0": "__label__3",
        "4.0": "__label__4",
        "5.0": "__label__5"
    }
    return labels[rate]


def convert2wakati(text, tagger):
    """ わかち書きに変換 """
    result = tagger.parse(text)
    return result


def main():
    tagger = MeCab.Tagger('-Owakati')

    with open(sys.argv[1]) as file:
        reader = csv.reader(file)
        for row in reader:
            label = convert2fasttext(row[0])
            comment = convert2wakati("".join(row[1:]), tagger)
            # comment = stopword(comment)

            print("{label}, {comment}\n".format(label=label, comment=comment), end="")


if __name__ == "__main__":
    main()

実行してfastText用のファイルを作成します。

python to_fasttext.py reviews.csv > reviews.lst

学習

さっそく作成したデータをfastTextで学習させてみます。
学習コードは下記です。

# -*- coding: utf-8 -*-
# train.py
import fasttext as ft
import sys

input_file = sys.argv[1]
output = sys.argv[2]

classifier = ft.supervised(input_file, output)

実行すると一瞬で学習が終了します。

python train.py reviews.lst reviews

検証1

検証コードを書いて、ちゃんと学習されているか検証してみます。

# -*- coding: utf-8 -*-
# estimate.py
import csv
import sys
from builtins import input

import MeCab
import fasttext as ft


def convert2wakati(text):
    """ わかち書きに変換 """
    tagger = MeCab.Tagger('-Owakati')
    result = tagger.parse(text)
    return result


def estimation(input, classifier):
    bow = convert2wakati(input)
    estimate = classifier.predict_proba(texts=[bow])[0]

    prob = {}
    for e in estimate:
        index = int(e[0][9:-1])
        prob[index] = e[1]

    score = 0.0
    for e in prob.keys():
        score += e * prob[e]
    return score  # 星の数を出力


def recall(classifier, csv_filename):
    """ recall """
    total_loss = 0.0
    count = 0
    with open(csv_filename) as file:
        reader = csv.reader(file)
        for row in reader:
            label = float(row[0])
            text = convert2wakati("".join(row[1:]))
            score = estimation(text, classifier)

            loss = (score - label) ** 2
            total_loss += loss
            count += 1
    total_loss = total_loss / count
    print("Total Loss: {loss:.2f}".format(loss=total_loss))


def main(classifier):
    while True:
        text = input(">")

        if text == "exit":
            print("bye!!")
            sys.exit(0)

        score = estimation(text, classifier)
        print("\nEvaluation = 星{score:.2f}つ\n".format(score=score))

if __name__ == "__main__":
    argvs = sys.argv

    if len(argvs) < 2:
        sys.exit(0)

    classifier = ft.load_model(argvs[1])
    if len(argvs) == 3:
        csv_filename = argvs[2]
        recall(classifier, csv_filename)
    else:
        main(classifier)

教師データをそのまま検証データとして利用し、簡単なMSE(平均二乗誤差)を算出してみます。

python estimate.py reviews.bin reviews.csv

Total Loss: 2.70

なんとなくある程度学習出来ているようです。

Stop Word

レビューコメントには、数字や接続詞、アルファベットのみなど分析に不要なデータも多く含まれるため、それらを除外すると精度が変化するか検証してみます。

csvからfastText形式に変換する際に、下記のように正規表現で除外しました。

def stopword(text):
    single = r"^[\"()/0-90-9ぁ-んァ-ン!!%%\[-ー{}()-~〜\u3001-\u303F\n]$"
    double = r"^[ぁ-ん]{2}$"
    number = r"^[0-9]+"
    english = r"^[a-zA-Z]+"
    words = text.split(" ")
    out_text = [word for word in words if len(word) > 1 and not re.match(single, word) and
                not re.match(double, word) and
                not re.match(number, word) and
                not re.match(english, word)]
    return " ".join(out_text)

先程と同様に、教師データのMSEを算出してみます。

python estimate.py reviews.bin reviews.csv

Total Loss: 2.33

Stop Word処理をした方が若干精度が良いようです。

検証2

教師データとして利用していない検証データでMSEを算出してみます。

Stop Word無し

Total Loss: 2.66

Stop Word有り

Total Loss: 2.72

Stop Word有りでも無しでもあまり変わらない(むしろ無しの方が精度が若干良い)という結果になりました。
Stop Word処理はやらない方が良いのかもしれません。

適当な文章を入力してみる

python estimate.py review.bin

input_result

何となくそれなりの結果が出てそうです。

まとめ

  • 星ごとの教師データ数にばらつきがあるため、精度が出ていない可能性がある。
  • fastTextの学習はとんでもなく早い。

今度時間がある時、CNNかLSTMを使った検証もしてみようかと思います。


AIQでは、私達と東京か札幌で一緒に働ける仲間を募集しています。
詳しくはこちら

私達と一緒にを様々な業界の未来を変えていきませんか?