エンジニア

機械学習で大量のテキストをカテゴリ別に分類してみよう!

投稿日:2017年10月10日 更新日:

こんにちはアドバンストテクノロジー部の@y-matsushitaです。
今回は機械学習を使った取り組みとして、手始めにfastTextを使ったテキストの分類について触れたいと思います。

fasttext.cc

fastTextとはFacebookが提供する単語のベクトル化とテキスト分類をサポートした機械学習ライブラリです。
fastTextという名前の通り動作が軽く早いのが特徴です。試しに使ってみたところ精度も良好で動作も軽かったのでご紹介させていただきます!
今回は試しに様々な情報が入り混じったTwitterの投稿内容を分類して「美容系」「エンタメ系」「暮らし系」情報の3パターンに分類してみます。
なお今回の記事ではPython 3.6.1を使用します。


fastTextを使ってできること

まず最初にfastTextを使った結果をお見せします。
『分類前』が処理前で『分類後』がfastTextを使って分類した結果です。

『分類前』
f:id:y-matsushita:20170929141107p:plain:w600
緑:美容系 ,
青:エンタメ系 ,
赤:暮らし系

上記のように分類前は色々なジャンルの投稿内容が入り混じった状態です。
これをfastTextで分類すると以下のような形になります。

→『分類後(美容系)』
f:id:y-matsushita:20170929141341p:plain:w600

→『分類後(エンタメ系)』
f:id:y-matsushita:20170929141501p:plain:w600

→『分類後(暮らし系)』
f:id:y-matsushita:20170929141532p:plain:w600

上記のように概ね文章を「美容系」「エンタメ系」「暮らし系」などに分類することができました。 *1
このように文章からカテゴリごとに自動分類したり、スパム的な投稿内容を検知したりもできます。
作成するモデル次第でかなり応用が効きそうな感じです。

また、単語のベクトル表現をつくることで、
SNSユーザの投稿内容からおすすめを紹介するレコメンド機能なども活用事例の代表です。


fastTextをインストール

fastTextはGitHubからダウンロード可能です。
以下のコマンドでダウンロードすると実行できるようになります。

$ git clone https://github.com/facebookresearch/fastText.git
$ cd fastText
$ make
$ pip install cython
$ pip install fasttext

学習用のテキストを用意する

学習用のテキストは過去にTwitterに投稿された「美容系」と「エンタメ系」と「暮らし系」のワードを含むツイートから作成します。

TwitterのAPI Keyを用意

Twitterの内容はAPIから取得します。
TwitterのAPI Keyを持っていない場合は予め用意する必要があります。
Twitter Application Management

MeCabをインストール

日本語は英語とは異なり単語同士がスペースで区切られていないため、分かち書きをする必要があります。
今回はMeCabを利用しました。
mecab-python3

MeCabをインストールしていない場合、以下のコマンドでインストールを行います。

$ brew install mecab mecab-ipadic
$ pip install mecab-python3

投稿の取得からテキスト出力までは以下のコードで行います。
TwitterのAPI Keyと取得したいカテゴリが含まれるキーワードを入力して取得します。

tweet_get.py
import re
import json
import MeCab
from requests_oauthlib import OAuth1Session

CK = "(用意したConsumer Keyを入力)"
CS = "(用意したConsumer Secretを入力)"
AT = "(用意したAccess Tokenを入力)"
AS = "(用意したAccess Token Secretを入力)"

API_URL = "https://api.twitter.com/1.1/search/tweets.json?tweet_mode=extended"
KEYWORD = "芸能 OR アニメ OR 漫画 OR TV OR ゲーム"            #エンタメ系のキーワードを入力
CLASS_LABEL = "__label__1"

def main():
    tweets = get_tweet()                #ツイートを取得
    surfaces = get_surfaces(tweets)     #ツイートを分かち書き
    write_txt(surfaces)                 #ツイートを書き込み

def get_tweet():
    """
    TwitterからKEYWORDに関連するツイートを取得
    """
    params = {'q' : KEYWORD, 'count' : 100}
    twitter = OAuth1Session(CK, CS, AT, AS)
    req = twitter.get(API_URL, params = params)
    results = []
    if req.status_code == 200:
        # JSONをパース
        tweets = json.loads(req.text)
        for tweet in tweets['statuses']:
            results.append(tweet['full_text'])
        return results
    else:
        # エラー
        print ("Error: %d" % req.status_code)

def get_surfaces(contents):
    """
    文書を分かち書きし単語単位に分割
    """
    results = []
    for row in contents:
        content = format_text(row)
        tagger = MeCab.Tagger('')
        tagger.parse('')
        surf = []
        node = tagger.parseToNode(content)
        while node:
            surf.append(node.surface)
            node = node.next
        results.append(surf)
    return results

def write_txt(contents):
    """
    評価モデル用のテキストファイルを作成する
    """
    try:
        if(len(contents) > 0):
            fileNema = CLASS_LABEL + ".txt"
            labelText = CLASS_LABEL + ", "

            f = open(fileNema, 'a')
            for row in contents:
                # 空行区切りの文字列に変換
                spaceTokens = " ".join(row);
                result = labelText + spaceTokens + "\n"
                # 書き込み
                f.write(result)
            f.close()

        print(str(len(contents))+"行を書き込み")

    except Exception as e:
        print("テキストへの書き込みに失敗")
        print(e)

def format_text(text):
    '''
    ツイートから不要な情報を削除
    '''
    text=re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", text)
    text=re.sub(r'@[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", text)
    text=re.sub(r'&[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", text)
    text=re.sub(';', "", text)
    text=re.sub('RT', "", text)
    text=re.sub('\n', " ", text)
    return text

if __name__ == '__main__':
    main()

取得後のテキストのフォーマットは以下の様に、見出しに__label__*と分類用のラベルを付けています。
この場合、__label__1に「美容系」、__label__2に「エンタメ系」、__label__3に「暮らし系」を設定しています。
分類ごとにCLASS_LABELとキーワードを変えて実行します。

__label__1,美容 室 の カラー と 自宅 で の カラー リング の 違い と は 。 。 。
__label__1,はちみつ に は 、 優れ た 保 湿 力 が あり ます 。 美容 に 優れ た 効果 が あり ます 。
__label__2,【 名 台詞 】 「 あきらめ たら そこ で 試合 終了 です よ … ? 」 某 人気 漫画 より
__label__2,主 に アニメ の 絵 や 漫画 を 描い たり 、 ブログ など の 活動 を し て い ます 。 pixiv で 漫画 を 投稿 し 、 ブログ で 発表 を 繰り返し て ます 良けれ ば 話しかけ て ください
__label__3,うち の ペット の 犬 が 食後 に 家 の 中 で 穴 を 掘ろ う と する ん だ けど 、 どうして だろ う 。
__label__3,もうすぐ 修学旅行 ! 高校 生活 の ひとつ で ある 行事 楽しん で き ま ー す

今回は約3000件*3の投稿内容を用意しました。
上記のコードでは一度に100件までしか取得できないため、
何回か時間を置いて繰り返し実行してデータをためてください。

取得直後のテキストはカテゴリごとにバラバラの状態なので、CATコマンドで結合させましょう。

$ cat __label__1.txt __label__2.txt __label__3.txt > model.txt

テキストからモデルを生成する

取得した「model.txt」からfastTextのモデルを生成します。
生成は以下のコードで行います。変換自体は1行で完結します。

learning.py
import sys
import fasttext as ft

argvs = sys.argv
input_file = argvs[1]
output_file = argvs[2]

classifier = ft.supervised(input_file, output_file)

input_fileに学習用のテキストファイル名、
output_fileに生成後のモデル名を設定します。

$ python learning.py model.txt model

生成が終わるとmodel.binというモデルが出力されます。


文章の分類

fastTextにモデルと新たに判断したいテキストを渡すと、
判定結果の分類とその判定が下される確率が取得できます。
判定結果が入力した文章に近いほど確率の値が高くなります。

prediction.py
import sys
import fasttext as ft
import MeCab

class predict:

    def __init__(self):
        # モデル読み込み
        self.classifier = ft.load_model('model.bin')

    def get_surfaces(self, content):
        """
        文書を分かち書き
        """
        tagger = MeCab.Tagger('')
        tagger.parse('')
        surfaces = []
        node = tagger.parseToNode(content)

        while node:
            surfaces.append(node.surface)
            node = node.next

        return surfaces

    def tweet_class(self, content):
        """
        ツイートを解析して分類を行う
        """
        words = " ".join(self.get_surfaces(content))
        estimate = self.classifier.predict_proba([words], k=3)[0][0]

        if estimate[0] == "__label__1,":
            print('美容系', estimate[1])
        elif estimate[0] == "__label__2,":
            print('エンタメ系', estimate[1])
        elif estimate[0] == "__label__3,":
            print('暮らし系', estimate[1])

if __name__ == '__main__':
    pre = predict()
    pre.tweet_class("".join(sys.argv[1:]))

実行結果

以下のようにコマンド入力し最後に判定するテキスト内容を貼り付けて実行します。

$ python prediction.py "判定するテキスト"

試しにTwitterから学習に使用していない適当なツイートで判定を行います。

python prediction.py "化粧水・乳液・美容液がひとつになった基礎化粧品が本日発売開始"
美容系 0.998047
python prediction.py "10月○日より劇場版公開予定!東京で舞台挨拶が行われました"
エンタメ系 0.654297
python prediction.py "お買い得な生鮮食品を毎日お届け。格安情報はこちら。"
暮らし系 0.8125

結構いい感じに分類してくれている気がします。
また取得した投稿3000件*3を学習用、100件*3を検証用に別で用意し実験したところ、 96.333%の精度で分類できました。*2


分類が難しい点

試していく中で分類がうまくいってないところがあったのでご紹介します。

のび太「あったかいふとんでぐっすりねる。こんな楽しいことがほかにあるか。」
(エンタメ系 0.443359 暮らし系 0.439453 美容系 0.115234)
エンタメ系も入ってますが、かなり暮らし系寄りですね。
登場人物のセリフとかになると分類はかなり難しくなりそうです。
またこのセリフにはありませんが健康などについて触れると、美容系も高くなってきます。

メジャーリーグでも活躍した某野球選手が美容整形をカミングアウト!スタジオは大騒然!
(美容系 0.996094 エンタメ系 1.95313e-08 暮らし系 1.95313e-08)
こちらは美容整形という部分に大きく反応して美容系に寄ってしまってます。
学習に使用したテキスト次第で特定のキーワードが強くなりすぎてしまうのかもしれません。


まとめ

fastTextを使って文章を「美容系」、「エンタメ系」、「暮らし系」に分類しました。
文章の分類だけでなくネガポジ判定や特定の単語に似たワードを抽出するなどにも使えるので、活用の幅は多そうです。
思いの外、簡単に実装できたのでチャレンジしてみてはいかがでしょうか!

*1:公開されているとはいえ一般の方のツイートを使うのは抵抗があったため記事内のツイートは一部架空のものを使用しています。

*2:学習用データと検証用のデータの取得方法が同じ場合での結果なので、実際に運用した場合の精度はもっと下がると思われます。

採用情報

ワンダープラネットでは、一緒に働く仲間を幅広い職種で募集しております。

-エンジニア
-

© WonderPlanet Inc.