Ahogrammer

Deep Dive Into NLP, ML and Cloud

教師なしで作る評価分析器

評価分析は自然言語処理の基礎技術でありながら実世界に広く応用されている技術です。たとえば、顧客の声を拾うために商品レビューを評価分析して肯定的なのか否定的なのか判断するのに使われています。また、情報抽出の技術と組み合わせて、文書のどの部分が肯定的/否定的なことを言っているのかを判定することもあります。

f:id:Hironsan:20180914094939j:plain 参照: An online form with built-in Sentiment Analysis

評価分析を行うためによく使われるのは教師あり学習による手法です。教師あり学習を用いた手法では、評価分析をする対象のテキストとその評価のマッピング機械学習アルゴリズムに学習させます。したがって、教師ありの手法を使うためにはラベルの付いたテキストを用意する必要があります。

本記事では、教師なしで評価分析器を作成する方法を紹介します。教師なしの手法のメリットとしては、ラベル付与済みのテキストを用意する必要がない点を挙げられます。記事は以下の内容で構成されています。

  • 実装するモデルの説明
  • モデルの実装

では、実装していきましょう。

今回実装するモデル

今回は教師無しで評判分析を行うモデルを構築します。具体的にはTurneyらが提案したモデルを構築します。このモデルは、2002年に提案されたかなり昔のモデルなのですが、評価分析のサーベイでは必ず言及されている有名なモデル(引用数5000超え!)です。Turneyらのモデルは以下の3ステップから構築されています。

  • フレーズの抽出
  • Semantic Orientationの計算
  • テキストの分類

フレーズの抽出ではレビュー文書から形容詞(JJ)か副詞(RB)を含む2単語のフレーズを以下の品詞パターンに従って抽出します。形容詞や副詞を含むフレーズを抽出する理由は、これらのフレーズがテキストの評価を表していると考えられるからです。たとえば、ラーメンのレビューなら「おいしいラーメン」や「とってもたまらない」といったフレーズを抽出できます。

f:id:Hironsan:20180914064419p:plain

フレーズを抽出した後、各フレーズに対してSemantic Orientation(SO)を計算します。SOというのはポジティブ/ネガティブを表す連続値の指標と考えていただければ良いと思います。このSOという指標は以下に示すように自己相互情報量(PMI)を使って定義されています。PMIを使うことで、2要素間の関連度を測ることができます。

PMI(phrase, word) = \displaystyle\log\frac{P(phrase, word)}{P(phrase)P(word)}

SO(phrase) = PMI(phrase, 'excellent') - PMI(phrase, 'poor')

要するにSOでは何をしているかというと、抽出したフレーズが良い意味を持つ単語と悪い意味を持つ単語のどちらに関連性が高いかを判断しています。 ここで問題となるのは、PMI中の各確率をどうやって計算するのかという点です。これらの確率は検索エンジンで単語あるいはフレーズを検索して得られた検索結果の件数を使って表します。それを踏まえてSOの式を書き直すと以下のように表せます。

SO(phrase) = \displaystyle\log\frac{\rm{hits(phrase\ NEAR\ excellent)}\ \rm{hits(poor)}}{\rm{hits(phrase\ NEAR\ poor)}\ \rm{hits(excellent)}}

実際にはゼロ除算を避けるために、hitsに0.01を加えて使います。

各フレーズに対してSOを計算できたら、その結果を使ってテキストを分類します。テキストを分類するために、抽出された各フレーズに対するSOの平均値を計算します。その平均値がプラスだったらポジティブ、マイナスだったらネガティブに分類します。

理屈の説明は以上です。では実装していきましょう。

Pythonによる実装

今回はぐるなびのレビューを分類してみます。以下のようなレビューをぐるなびの応援口コミAPIから取得して使います。

うすい色のスープにやわやわなチャーシュー、そしてやわらかいのにコシがあってスープによく絡むたまごめん。ご馳走ではないかもしれない。だが胃に、心に、染み渡る。佐野ラーメンで育ったわたしには所謂こういう中華そば的なラーメンがとってもたまらない。佐野ラーメンがすきなひとにはぜひ味わっていただきたい一品。ソースかつ丼もまた、ソースが甘くて美味しかった。これもまたいもふらいを思い起こさせる。。。

フレーズの抽出

まずはフレーズの抽出をします。フレーズを抽出するためには分かち書きと品詞情報が必要なので、形態素解析を行います。日本語形態素解析JanomeのFilter機能を使ってパターンにマッチするフレーズを抽出します。そのためのフィルターは以下のように書くことができます。

import copy

from janome.tokenfilter import TokenFilter


class PhraseFilter(TokenFilter):

    def apply(self, tokens):
        tokens = list(tokens)
        tokens = self.add_sentinel(tokens)
        for i, token1 in enumerate(tokens[:-2]):
            token2 = tokens[i + 1]
            token3 = tokens[i + 2]
            pos1 = token1.part_of_speech
            pos2 = token2.part_of_speech
            pos3 = token3.part_of_speech
            if self.satisfy_patterns(pos1, pos2, pos3):
                token1.surface += token2.surface
                token1.base_form += token2.base_form
                token1.reading += token2.reading
                token1.phonetic += token2.phonetic
                yield token1

    def satisfy_patterns(self, pos1, pos2, pos3):
        if pos1.startswith('形容詞') and pos2.startswith('名詞'):
            return True
        if pos1.startswith('副詞') and pos2.startswith('形容詞') and not pos3.startswith('名詞'):
            return True
        if pos1.startswith('形容詞') and pos2.startswith('形容詞') and not pos3.startswith('名詞'):
            return True
        if pos1.startswith('名詞') and pos2.startswith('形容詞') and not pos3.startswith('名詞'):
            return True
        if pos1.startswith('副詞') and pos2.startswith('動詞'):
            return True
        return False

    def add_sentinel(self, tokens):
        try:
            token = copy.deepcopy(tokens[0])
            token.part_of_speech = 'EOS'
            tokens.append(token)
        except IndexError:
            pass
        return tokens

PhraseFilterクラスで重要なのはsatisfy_pattensメソッドです。このメソッドであらかじめ定義した品詞パターンにマッチするトークンを判定しています。また、処理を簡単にするためにadd_sentinelメソッドでtokensの最後に要素を追加しています。

作成したフィルターを使って、実際のレビューからフレーズを抽出してみましょう。レビューのテキストに対して、作成したフィルターを使って形態素解析を行います。以下のコードでは、Analyzerを初期化する際に、作成したフィルターを渡しています。これにより、フィルターに合致するトークンだけを返すことができます。

token_filters = [PhraseFilter()]
a = Analyzer(token_filters=token_filters)
for token in a.analyze(text):
    print(token.surface)

フレーズを抽出したところ以下のような結果になりました。評価に関係するフレーズを取り出せてる感じがします。

うすい色
よく絡む
とってもたまらない
ぜひ味っ

Semantic Orientationの計算

では次に、抽出した各フレーズに対してSemantic Orientationを計算しましょう。英語の場合は良い意味の単語として「excellent」、悪い意味の単語として「poor」を使っていました。今回は日本語なので、それぞれ「すばらしい」と「ひどい」を使うことにしてみましょう。検索エンジンとしてGoogleを使ってSOを計算したところ、以下の結果を得ることができました。なお、今回はNEARは考慮していません。

f:id:Hironsan:20180914092605p:plain

今回は手動で検索しましたが、GoogleCustom Search APIを使えば検索結果の件数を取得できるため、SOを自動で計算できます。

テキストの分類

各フレーズに対するSOを計算できたので、その結果を使ってテキストを分類します。計算済みのSOの平均値を見ると、0.164です。したがって、今回のレビューはポジティブに分類されることがわかります。

感想

  • 分量のあるテキストに適した手法と感じた
  • 日本語の場合、助詞を考慮したほうが良さそう
  • 良い単語と悪い単語の決め方が難しい
  • ヒット数を信用できるのか疑問

実装簡単だし手軽だけど、個人的には教師ありのほうがいいかな(´・ω・`)

おわりに

本記事では教師無しで評価分析を行う方法を紹介し、実装しました。この記事が皆様のお役に立てば幸いです。

私のTwitterアカウントでも機械学習自然言語処理に関する情報をつぶやいています。

@Hironsan

この分野にご興味のある方のフォローをお待ちしています。

参考資料