Twitter Stream APIデータに対して初歩的な感情分析を試みる。

  • 240
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

Twitter解析を行ううえでやってみたいことの1つとして感情分析があるのではないかと思います。(ですよね?)色々な手法があると思いますが、まずはその一番簡単な例から初めてだんだん高度に(できれば)していくというのを見ていきたいと思います。

分析対象データは今回もTwitterとします。ただ、いままではTwitter REST APIsで取得していましたが、今回はTwitter Stream APIでじゃんじゃかTwitterデータをインポートしていき、それに対して感情分析結果を数値化して合わせてデータベースに格納する、ということをやってみたいと思います。

Twitterデータをmongodbに取得する当たりの説明は以前の記事で行っていますので、よければご参照ください。

1. Twitter Stream APIからデータを取得しmongoDBに格納する。

1-1. 準備的なこと

まずは下ごしらえです。
各種ライブラリのインポート、ユーティリティー的な関数の宣言、DBへの接続を行います。

from requests_oauthlib import OAuth1Session
from requests.exceptions import ConnectionError, ReadTimeout, SSLError
import json, time, exceptions, sys, datetime, pytz, re, unicodedata, pymongo
import oauth2 as oauth
import urllib2 as urllib
import MeCab as mc
from collections import defaultdict
from pymongo import MongoClient
from httplib import IncompleteRead
import numpy as np

import logging
from logging import FileHandler, Formatter
import logging.config

connect = MongoClient('localhost', 27017)
db = connect.word_info
posi_nega_dict = db.posi_nega_dict
db2 = connect.twitter
streamdata = db2.streamdata

def str_to_date_jp(str_date):
    dts = datetime.datetime.strptime(str_date,'%a %b %d %H:%M:%S +0000 %Y')
    return pytz.utc.localize(dts).astimezone(pytz.timezone('Asia/Tokyo'))

def mecab_analysis(sentence):
    t = mc.Tagger('-Ochasen -d /usr/local/Cellar/mecab/0.996/lib/mecab/dic/mecab-ipadic-neologd/')
    sentence = sentence.replace('\n', ' ')
    text = sentence.encode('utf-8') 
    node = t.parseToNode(text) 
    result_dict = defaultdict(list)
    for i in range(140):  # ツイートなのでMAX140文字
        if node.surface != "":  # ヘッダとフッタを除外
            word_type = node.feature.split(",")[0]
            if word_type in ["形容詞", "動詞","名詞", "副詞"]:
                plain_word = node.feature.split(",")[6]
                if plain_word !="*":
                    result_dict[word_type.decode('utf-8')].append(plain_word.decode('utf-8'))
        node = node.next
        if node is None:
            break
    return result_dict

def logger_setting():
    import logging
    from logging import FileHandler, Formatter
    import logging.config

    logging.config.fileConfig('logging_tw.conf')
    logger = logging.getLogger('filelogger')
    return logger

logger = logger_setting()

KEYS = { # 自分のアカウントで入手したキーを下記に記載
        'consumer_key':'**********',
        'consumer_secret':'**********',
        'access_token':'**********',
        'access_secret''**********',
       }



今回、ログはLoggerをつかってファイル出力をするようにしています。ログの出力設定ファイルは下記です。

logging_tw.conf
# logging_tw.conf

[loggers]
keys=root, filelogger

[handlers]
keys= fileHandler 

[formatters]
keys=logFormatter

[logger_root]
level=DEBUG
handlers=fileHandler

[logger_filelogger]
level=DEBUG
handlers=fileHandler
qualname=filelogger
propagate=0

[handler_fileHandler]
class=handlers.RotatingFileHandler
level=DEBUG
formatter=logFormatter
args=('logging_tw.log',)

[formatter_logFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=

1-2. 日本語評価極性辞書のダウンロードと永続化

東北大学の乾先生・岡崎先生の研究室で作成されている日本語評価極性辞書を使って感情を数値化しますので、まずは

  • 日本語評価極性辞書(用言編)ver.1.0(2008年12月版) wago.121808.pn
  • 日本語評価極性辞書(名詞編)ver.1.0(2008年12月版) pn.csv.m3.120408.trim

ここからダウンロードして.pyと同じフォルダに格納します。

日本語評価極性辞書(用言編)に関しては、ポジの用語は 1、ネガの用語は -1 と数値化してmongodbにインポートします。
日本語評価極性辞書(名詞編)については、pの用語は 1、eの用語は 0、nの用語は -1 と数値化して同じくmongodbにインポートします。
下記がそのコードとなります。

# 単語のポジ・ネガ辞書のmongoDBへのインポート

# 日本語評価極性辞書(用言編)ver.1.0(2008年12月版)をmongodbへインポート
# ポジの用語は 1 ,ネガの用語は -1 と数値化する
with open("wago.121808.pn.txt", 'r') as f:
    for l in f.readlines():
        l = l.split('\t')
        l[1] = l[1].replace(" ","").replace('\n','')
        value = 1 if l[0].split('(')[0]=="ポジ" else -1
        posi_nega_dict.insert({"word":l[1].decode('utf-8'),"value":value})


# 日本語評価極性辞書(名詞編)ver.1.0(2008年12月版)をmongodbへインポート
# pの用語は 1 eの用語は 0 ,nの用語は -1 と数値化する
with open("pn.csv.m3.120408.trim", 'r') as f:
    for l in f.readlines():
        l = l.split('\t')

        if l[1]=="p":
            value = 1
        elif l[1]=="e":
            value = 0
        elif l[1]=="n":
            value = -1

        posi_nega_dict.insert({"word":l[0].decode('utf-8'),"value":value})  

1-3. 感情度を数値化する処理

1-2で単語毎の感情値をデータベース化できたので、文章を感情値化できるような処理を入れていきます。ただし今回は「初歩的な」ということで何も処理を行わず、

  1. 単純に文中に含まれる単語をそれぞれ日本語評価極性辞書に存在するか確認
  2. 存在する場合はその数値を利用する
  3. 下記の式に従い、文章の感情値を算出する。($x_i$は日本語評価極性辞書に存在する単語の感情値、$n$は日本語評価極性辞書に存在する単語数)
{\rm sentiment\, value\, of\, the\, sentence} \, = \, \frac{1}{n}\sum_{i=1}^{n} x_i 

なので、-1 〜 1の間で文章の単語数によらない形で感情値を導出でき、文章間の比較が可能になる。


# 感情度の設定(検索高速化のためハッシュ検索ができるよう辞書オブジェクトに入れ込む)
pn_dict = {data['word']: data['value'] for data in posi_nega_dict.find({},{'word':1,'value':1})}

def isexist_and_get_data(data, key):
    return data[key] if key in data else None

# -1 〜 1の範囲で与えられた文章(単語リスト)に対する感情値を返す。(1: 最もポジ、-1:最もネガ)
def get_setntiment(word_list):
    val = 0
    score = 0
    word_count = 0
    val_list = []
    for word in word_list:
        val = isexist_and_get_data(pn_dict, word)
        val_list.append(val)
        if val is not None and val != 0: # 見つかればスコアを足し合わせて単語カウントする
            score += val
            word_count += 1

    logger.debug(','.join(word_list).encode('utf-8'))       
    logger.debug(val_list)
    return score/float(word_count) if word_count != 0. else 0.

1-4. Twitter Stream APIからデータのダウンロード

Twitter Stream APIよりツイートデータをダウンロードするコードです。
ツイートをダウロードしながらMeCabで形態素解析をして単語ごとにバラバラにして名詞、動詞、形容詞、副詞ごとにリストにします。いわゆるBag of Wordsですね。

で、その単語たちに対してさきほど定義した関数get_setntiment()で感情値を導出、これと合わせてmongodbに格納します。

# ----- Streamデータのインポート ---------#
consumer = oauth.Consumer(key=KEYS['consumer_key'], secret=KEYS['consumer_secret'])
token = oauth.Token(key=KEYS['access_token'], secret=KEYS['access_secret'])

url = 'https://stream.twitter.com/1.1/statuses/sample.json'
params = {}

request = oauth.Request.from_consumer_and_token(consumer, token, http_url=url, parameters=params)
request.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, token)
res = urllib.urlopen(request.to_url())

def get_list_from_dict(result, key):
    if key in result.keys():
        result_list = result[key]
    else:
        result_list = []
    return result_list

cnt = 1
try:
    for r in res:
        data = json.loads(r)
        if 'delete' in data.keys():
            pass
        else:    
            if data['lang'] in ['ja']: #['ja','en','und']:
                result = mecab_analysis(data['text'].replace('\n',''))

                noun_list      = get_list_from_dict(result, u'名詞')
                verb_list      = get_list_from_dict(result, u'動詞')
                adjective_list = get_list_from_dict(result, u'形容詞')
                adverb_list    = get_list_from_dict(result, u'副詞')

                item = {'id':data['id'], 'screen_name': data['user']['screen_name'], 
                        'text':data['text'].replace('\n',''), 'created_datetime':str_to_date_jp(data['created_at']),\
                       'verb':verb_list, 'adjective':adjective_list, 'noun': noun_list, 'adverb':adverb_list}
                if 'lang' in data.keys():
                    item['lang'] = data['lang']
                else:
                    item['lang'] = None

                #感情分析結果を追加 ####################
                word_list = [word for k in result.keys() for word in result[k] ]
                item['sentiment'] = get_setntiment(word_list)

                streamdata.insert(item)
                if cnt%1000==0:
                    logger.info("%d, "%cnt)
                cnt += 1
except IncompleteRead as e:
    logger.error( '=== エラー内容 ===')
    logger.error(  'type:' + str(type(e)))
    logger.error(  'args:' + str(e.args))
    logger.error(  'message:' + str(e.message))
    logger.error(  'e self:' + str(e))
    try:
        if type(e) == exceptions.KeyError:
            logger.error( data.keys())
    except:
        pass
except Exception as e:
    logger.error( '=== エラー内容 ===')
    logger.error( 'type:' + str(type(e)))
    logger.error( 'args:' + str(e.args))
    logger.error( 'message:' + str(e.message))
    logger.error( 'e self:' + str(e))
    try:
        if type(e) == exceptions.KeyError:
            logger.error( data.keys())
    except:
        pass 
except:
    logger.error( "error.")

logger.info( "finished.")

ここまでが単純に単語ごとに感情値を割り当てて平均をとるという単純な方法で分析を行う方法でした。
今後の展開としては、さらなる前処理としてSpam分類、また、現状だと単語間の関連性を考慮に入れた処理が課題となると考えています
特に「可愛くない」のような単語は「可愛い」と「ない」に分かれて、「ない」が否定のため、「可愛い」が+1.0のポジティブ表現を「ない」で打ち消して-1.0とすることが自然なのですが、現状は「可愛い」だけが処理され+1.0となり真逆の結果となっているのです。

これを正しく処理するために「係り受け解析」という手法で「ない」がどの単語にかかっているかを関連づけ、解釈するということが必要になります。次の節でその係り受け分析ライブラリCaboChaをまずはインストールする方法を説明したいと思います。

2. 係り受け解析

2-1. 係り受け解析ライブラリCaboChaのインストール

ということで係り受け解析ライブラリCaboCha http://taku910.github.io/cabocha/のMacへのインストールを扱いたいと思います。
なかなかインストールに手間取ったので、参考になればと思っています。

CaboChaのダウンロード
https://drive.google.com/folderview?id=0B4y35FiV1wh7cGRCUUJHVTNJRnM&usp=sharing#list

CaboChaを入れるにはCRF+というライブラリが必要となります。
CRF+のページ
http://taku910.github.io/crfpp/#install

CRF+ダウンロード
https://drive.google.com/folderview?id=0B4y35FiV1wh7fngteFhHQUN2Y1B5eUJBNHZUemJYQV9VWlBUb3JlX0xBdWVZTWtSbVBneU0&usp=drive_web#list

ダウンロードした後は解凍してmake & installします。いくつか必要な環境変数やライブラリがあるのでその適用についても下記で記載しています。

tar zxfv CRF++-0.58.tar
cd CRF++-0.58
./configure 
make
sudo make install

export LIBRARY_PATH="/usr/local/include:/usr/local/lib:"
export CPLUS_INCLUDE_PATH="/usr/local/include:/opt/local/include"
export OBJC_INCLUDE_PATH="/usr/local/include:/opt/local/lib"

brew tap homebrew/dupes
brew install libxml2 libxslt libiconv
brew link --force libxml2
brew link --force libxslt
brew link libiconv —force

tar zxf cabocha-0.69.tar.bz2
cd cabocha-0.69
./configure --with-mecab-config=`which mecab-config` --with-charset=UTF8
make
make check
sudo make install

#[output: install information]
#.././install-sh -c -d '/usr/local/share/man/man1'
#/usr/bin/install -c -m 644 cabocha.1 '/usr/local/share/man/man1'
#./install-sh -c -d '/usr/local/bin'
#/usr/bin/install -c cabocha-config '/usr/local/bin'
#./install-sh -c -d '/usr/local/etc'
#/usr/bin/install -c -m 644 cabocharc '/usr/local/etc'

cd cabocha-0.69/python
python setup.py install

cp build/lib.macosx-10.10-intel-2.7/_CaboCha.so /Library/Python/2.7/site-packages
cp build/lib.macosx-10.10-intel-2.7/CaboCha.py /Library/Python/2.7/site-packages

上記のインストール方法は下記のサイトを参考にさせていただきながら作成しました。

CaboChaインストールの参考にさせていただいたサイト

http://qiita.com/nezuq/items/f481f07fc0576b38e81d#1-10
http://hotolab.net/blog/mac_mecab_cabocha/
http://qiita.com/t_732_twit/items/a7956a170b1694f7ffc2
http://blog.goo.ne.jp/inubuyo-tools/e/db7b43bbcfdc23a9ff2ad2f37a2c72df
http://qiita.com/t_732_twit/items/a7956a170b1694f7ffc2

2-2. CaboChaお試し

お試しテキストで係り受け解析を試してみます。

import CaboCha

c = CaboCha.Parser()

sentence = "漱石はこの本を龍之介を見た女性に渡した。"

tree =  c.parse(sentence)

print tree.toString(CaboCha.FORMAT_TREE)
print tree.toString(CaboCha.FORMAT_LATTICE)

このコードの実行結果は下記です。

output
  漱石は-----------D
      この-D       |
        本を---D   |
      龍之介を-D   |
            見た-D |
            女性に-D
            渡した。
EOS

* 0 6D 0/1 -2.475106
漱石  名詞,固有名詞,人名,名,*,*,漱石,ソウセキ,ソーセキ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
* 1 2D 0/0 1.488413
この  連体詞,*,*,*,*,*,この,コノ,コノ
* 2 4D 0/1 0.091699
本 名詞,一般,*,*,*,*,本,ホン,ホン
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
* 3 4D 0/1 2.266675
龍之介   名詞,固有名詞,人名,名,*,*,龍之介,リュウノスケ,リューノスケ
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
* 4 5D 0/1 1.416783
見 動詞,自立,*,*,一段,連用形,見る,ミ,ミ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
* 5 6D 0/1 -2.475106
女性  名詞,一般,*,*,*,*,女性,ジョセイ,ジョセイ
に 助詞,格助詞,一般,*,*,*,に,ニ,ニ
* 6 -1D 0/1 0.000000
渡し  動詞,自立,*,*,五段・サ行,連用形,渡す,ワタシ,ワタシ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
。 記号,句点,*,*,*,*,。,。,。
EOS

"*" がある行が分析結果でそれに続く幾つかの単語が文節になります。

*の次が「文節番号」を表します。
その次は、係り先の文節番号で、係り先がない場合は-1になります。"D"は気にしなくても良いようです。

次の2つの数字は主辞/機能語の位置です

最後の数値は係り関係のスコア係りやすさの度合を示します. 一般に大きな値ほど係りやすいことを表すようです。

なので、最初の文節は0の「漱石は」となり、係り先は6Dなので「渡した」となります。

今回の記事では、係り受け解析ライブラリのCaboChaのインストールまでを行いました。次回の記事でこれをツイートデータに適用することを行っていきたいと思います。

参考文献等

日本語評価極性辞書 - 乾・岡崎研究室 - Tohoku University
小林のぞみ,乾健太郎,松本裕治,立石健二,福島俊一. 意見抽出のための評価表現の収集. 自然言語処理,Vol.12, No.3, pp.203-222, 2005.
東山昌彦, 乾健太郎, 松本裕治, 述語の選択選好性に着目した名詞評価極性の獲得, 言語処理学会第14回年次大会論文集, pp.584-587, 2008.