負のオーラを自動検出したい
前回のエントリで、著作権侵害にあたる違法アプロード動画をTwitterで拡散してしまっている懸念を考えて、YouTube動画のリンクが貼ってあるツイートをまとめて削除しました。
前回のエントリでも言いましたが、著作権侵害モノ以外にも、「残しておくとまずいツイート」は色々ある可能性があり、たとえば誹謗中傷の類いがあるかと思います。誹謗中傷ツイートを自動抽出する方法はにわかには思いつきませんが、たぶん「クソ」とか「死ね」とか「バカ」とかそういう悪口の辞書が必要になりそうです。
ところで、言語データの分析手法として、単語ごとに感情特性を評価した辞書というものがあちこちで作られていまして、これを使ってツイートがどのような感情を帯びているか分析するということが、よくやられています。Yahoo!がそういうツールを提供してたりもします。
みんなの気持ちがわかっちゃう!? リアルタイム検索の感情分析がバージョンアップ - Yahoo!検索 スタッフブログ
ひょっとしたら、こういう手法を用いて、自分のツイートの中から「負のオーラ」が漂っているものを自動的に抽出し、削除するというアプローチがあり得るかもしれません。
というわけで今日は簡単な感情分析を行ってみました。
最初に言っておきますが、今回やったような単純な手法では精度が低くて使い物にはなりません。あくまでちょっと試しにやってみました程度のアウトプットになっております。
しかしそれでも、取っ掛かりとして手を動かして解析してみるというのは、色々知識が広がるものではあります。
さまざまな辞書
感情分析に使う辞書ですが、私が今回使ったのは、東工大の高村教授が作って公開されている「PN Table」というやつです。
PN Table
この辞書の中身は、
めでたい:めでたい:形容詞:0.999645
賢い:かしこい:形容詞:0.999486
善い:いい:形容詞:0.999314
適す:てきす:動詞:0.999295
過小:かしょう:名詞:-0.942846
塗る:ぬる:動詞:-0.943453
器量負け:きりょうまけ:名詞:-0.943603
固まる:かたまる:動詞:-0.943991
こんな感じで、単語に対応する極性情報が-1〜+1の間で割り当てられており、-1に近いほどネガティブ、+1に近いほとポジティブということになっています。後述するように、ゼロがニュートラルと言っていいのかはよく分かりません。
日本語で似たような辞書はこれ以外にもありまして、たとえば東北大の乾・岡崎研究室のページで公開されている「日本語評価極性辞書」というものがあります。
Open Resources/Japanese Sentiment Polarity Dictionary - 東北大学 乾・岡﨑研究室 / Communication Science Lab, Tohoku University
他には、Yahoo!JAPAN研究所の鍜治伸裕さんという方が作られた「Polar Phrase Dictionary」というのがあり、東大のサイトにそのページがありますが、ダウンロード可能なものとして公開されてるわけではないようです。
Polar Phrase Dictionary
今回使ったPN Tableの作成に関する高村教授らの論文は以下の場所で読めます。
高村大也,乾孝司,奥村学(2006).スピンモデルによる単語の感情極性抽出.情報処理学会論文誌ジャーナル,Vol.47,No.02,pp.627-637,2006.
ポジとかネガとかのことを「極性」と呼んでいるわけですが、PN Tableは根性で大量の単語に極性を割り振っていったわけではなく、大部分の単語の極性情報が機械的に導出されています。
物理学の理論を応用したモデルの詳細は、難しくて理解できねーと思いよく読んでませんが、「各電子のスピンは,上向きと下向きのうちどちらかの値をとり,隣り合ったスピンは同じ値をとりやすい.我々は,各単語を電子と見なし,単語の感情極性をスピンの向きと見なす.関連する単語ペアを連結することにより語彙ネットワークを構築し,これをスピン系と見なす.」そうです(汗)
その利点として、「我々のモデルでは,平均場近似により語彙ネットワーク上の単語の感情極性が大域的に決定される.このような大域的な最適化を用いるからこそ,語釈文やコーパスのような,シソーラスと比べてノイズが含まれやすい(すなわち,隣り合っていても同じ極性を持たないことが起こりやすい)リソースを取り入れることが可能になるのである.最短距離を利用した手法や単純なブートストラッピングを利用した手法のような既存手法では,そのようなリソースを取り入れることはできない.」らしいです(汗)(汗)
私にはよく分かりませんが、ともかくそういう理論物理学にヒントを得たモデルによって、まず、辞書・シソーラス・コーパスから「語彙ネットワーク」を形成しておき、そこにgood/badなどすでに判明している極性情報を注入してやることで、ネットワーク内の語彙に極性情報が伝搬されて、自動的に極性辞書ができあがるというプロセスのようです。たぶん。なんか近未来的です。
論文の冒頭の先行研究レビューの部分をみると、単語の感情特性を求めるためにこれまでどのような手法が提案されてきたかも概観できて参考になります。
自分のツイートを評価してみる作業
さて、PN Tableを使って自分のツイートを実際に評価してみます。
今回は、とりあえず何となく数値っぽいものを計算するところまで行きたかったので、単純な処理だけやりました。
前回のエントリでも使いましたが、自分のツイート全件を処理するときは、Twitterの公式サイトからダウンロードできる全ツイート履歴のファイルを使うのが良いです。
これをPandasのデータフレームとして取り込むところから分析がスタートします。各種モジュールも最初にインポートしておきます。
# モジュールのインポート import re import csv import time import pandas as pd import matplotlib.pyplot as plt import MeCab import random # tweets.csvの読み込み tw_df = pd.read_csv('tweets.csv', encoding='utf-8')
このテーブルには、
- tweet_id # ツイートごとのID(いわゆるstatus_id)
- in_reply_to_status_id
- in_reply_to_user_id
- timestamp # 投稿日時
- source # 投稿に用いたデバイス
- text # 本文
- retweeted_status_id
- retweeted_status_user_id
- retweeted_status_timestamp
- expanded_urls # リンクが貼られている場合の、省略しない形のURL
という10個のフィールドがありますが、今回必要とするのはtweet_idとtextだけです。
次に、PN TableもPandasで読み込みます。先ほども例示したように、
めでたい:めでたい:形容詞:0.999645
賢い:かしこい:形容詞:0.999486
善い:いい:形容詞:0.999314
適す:てきす:動詞:0.999295
過小:かしょう:名詞:-0.942846
塗る:ぬる:動詞:-0.943453
器量負け:きりょうまけ:名詞:-0.943603
固まる:かたまる:動詞:-0.943991
というような内容になっているので、read_csvのオプションで区切り文字を「:」と指定して読み込めばいいかと思います。*1
# PN Tableを読み込み # パスは各自適当なものになります pn_df = pd.read_csv('dictionary/PN_Table/pn_ja.dic.txt',\ sep=':', encoding='utf-8', names=('Word','Reading','POS', 'PN') )
まず、個々のツイートをMeCabで形態素解析して、単語に分けるとともにその基本形表記を取得します。MeCabの導入方法等は過去のエントリを参照してください。
基本形を取得するのは、実際の文中に登場する未然形や連用形のように活用された形だと、極性辞書をサーチすることができないからです。
MeCab Pythonでふつうに形態素解析をする場合、返ってくる情報は以下のような感じになります。
>>> print(m.parse('STAP細胞はあります。')) STAP 名詞,固有名詞,組織,*,*,*,* 細胞 名詞,一般,*,*,*,*,細胞,サイボウ,サイボー は 助詞,係助詞,*,*,*,*,は,ハ,ワ あり 動詞,自立,*,*,五段・ラ行,連用形,ある,アリ,アリ ます 助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス 。 記号,句点,*,*,*,*,。,。,。 EOS
1行1語になっていることが分かります。最後にEOSという終了記号が付いて、さらに空行が1行ついてきます。
これを後々どうやって扱うかなのですが、とりあえず私は深く考えずに、各行をdict型のデータに格納して、リストで連結しておくことにしました。
# MeCabインスタンス作成 m = MeCab.Tagger('') # 指定しなければIPA辞書 # -----テキストを形態素解析して辞書のリストを返す関数----- # def get_diclist(text): parsed = m.parse(text) # 形態素解析結果(改行を含む文字列として得られる) lines = parsed.split('\n') # 解析結果を1行(1語)ごとに分けてリストにする lines = lines[0:-2] # 後ろ2行は不要なので削除 diclist = [] for word in lines: l = re.split('\t|,',word) # 各行はタブとカンマで区切られてるので d = {'Surface':l[0], 'POS1':l[1], 'POS':l[2], 'BaseForm':l[7]} diclist.append(d) return(diclist)
これによって、1つのツイート本文が以下のような情報に変換されます。見やすくするために改行を入れますが、
[ {'POS': '固有名詞', 'POS1': '名詞', 'BaseForm': '*', 'Surface': 'STAP'}, {'POS': '一般', 'POS1': '名詞', 'BaseForm': '細胞', 'Surface': '細胞'}, {'POS': '係助詞', 'POS1': '助詞', 'BaseForm': 'は', 'Surface': 'は'}, {'POS': '自立', 'POS1': '動詞', 'BaseForm': 'ある', 'Surface': 'あり'}, {'POS': '*', 'POS1': '助動詞', 'BaseForm': 'ます', 'Surface': 'ます'}, {'POS': '句点', 'POS1': '記号', 'BaseForm': '。', 'Surface': '。'} ]
こういう感じのリストです。
品詞は何かに使うかもと思って一応取得したんですが、結局今回は使いませんでした。しかし修正するのが面倒なのでこのままにしておきます。
あとで拡張していく時にも使うかもしれません。実際、1つの文章を形態素解析して「辞書のリスト」にすることにしておけば、後で色々使いまわせるような気もします。
次に、上で得られた単語ごとのdict型データに、PN Tableから取った極性値を項目として追加したいと思います。
最初にPN TableをPandasデータフレームとして読み込んであったので、ふつうに考えたらこのデータフレームを検索してPN値を取ってくればいいということになります。
たとえば、
pn_df.loc[pn_df.Word == '細胞', 'PN']
というような処理を繰り返せばPN値は取得してこれるのですが、この方法だと死ぬほど時間がかかります。
最初、この方法で1万3000件のツイートの解析をやってみたのですが、CPU使用率が98.8%になり、処理がなかなか終わりませんでした。シャドウバースを2試合やっても終わらなかったので、外に出て剣道の素振りをして帰ってきたらようやく終わっていました。なので、たぶん30分以上はかかったと思います。
そこで、Pandasのデータフレームを文字列で検索するのは非常に時間がかかるので、PN Table自体を{'単語':PN値, '単語':PN値, '単語':PN値...}という形のdict型データに変換した上で、単語をキーとしてアクセスしてPN値を取ってくる方法に変更したら約8秒で終わりました。8分ではなく8秒です。30分→8秒。
また、感情辞書をこういう形にしておくことにすれば、他の辞書を使った分析へと拡張するのもやりやすいような気がしました。
# PN Tableをデータフレームからdict型に変換しておく word_list = list(pn_df['Word']) pn_list = list(pn_df['PN']) # 中身の型はnumpy.float64 pn_dict = dict(zip(word_list, pn_list)) # 形態素解析結果の単語ごとdictデータにPN値を追加する関数 def add_pnvalue(diclist_old): diclist_new = [] for word in diclist_old: base = word['BaseForm'] # 個々の辞書から基本形を取得 if base in pn_dict: pn = float(pn_dict[base]) # 中身の型があれなので else: pn = 'notfound' # その語がPN Tableになかった場合 word['PN'] = pn diclist_new.append(word) return(diclist_new)
PN Tableに載っていない語をどうするかなのですが、分析上は無視したいと思います。ゼロを割り当ててしまうとPN Table上で実際にゼロちょうどと評価されている単語(「週末」「巨体」「セレナーデ」など20語ある)なのか、載ってなかった単語なのかの区別がつかないので、適当にnotfoundと書いておきました。
これで各ツイートが以下のような形式のデータになります。
>>> test_text = 'STAP細胞はあります。' >>> dl_test = get_diclist(test_text) >>> dl_test = add_pnvalue(dl_test) >>> print(dl_test) [ {'PN': 'notfound', 'POS': '固有名詞', 'POS1': '名詞', 'BaseForm': '*', 'Surface': 'STAP'}, {'PN': -0.746254, 'POS': '一般', 'POS1': '名詞', 'BaseForm': '細胞', 'Surface': '細胞'}, {'PN': 'notfound', 'POS': '係助詞', 'POS1': '助詞', 'BaseForm': 'は', 'Surface': 'は'}, {'PN': 'notfound', 'POS': '自立', 'POS1': '動詞', 'BaseForm': 'ある', 'Surface': 'あり'}, {'PN': 'notfound', 'POS': '*', 'POS1': '助動詞', 'BaseForm': 'ます', 'Surface': 'ます'}, {'PN': 'notfound', 'POS': '句点', 'POS1': '記号', 'BaseForm': '。', 'Surface': '。'} ]
あとはPN値の平均をとるだけ(上記の文だとPN値を持った語が1つしかありませんが)なので、正直こんなゴツい形式のデータにする必要なかったと思いますが、先ほども述べたように「何かに使うかも」と思った経緯からこんな形になっております。
# 各ツイートのPN平均値をとる関数 def get_pnmean(diclist): pn_list = [] for word in diclist: pn = word['PN'] if pn != 'notfound': pn_list.append(pn) # notfoundだった場合は追加もしない if len(pn_list) > 0: # 「全部notfound」じゃなければ pnmean = mean(pn_list) else: pnmean = 0 # 全部notfoundならゼロにする return(pnmean)
全部notfound、つまりPN Tableに載っている単語を1語も含まないツイートを0点と評価するのはよく考えたら適切ではなく、分析から除外すべきですが、やりなおしが面倒なのでコードはこのままにしておきますw
ここまでできれば、あとはツイートの1件1件に対して、「形態素解析」「PN値の追加」「PNの平均値の算出」を繰り返していき、1つのリストにまとめます。
# pn値のリストを作成(一応時間を測りました) start_time = time.time() ### ▼時間計測▼ ### pnmeans_list = [] for tw in tw_df['text']: dl_old = get_diclist(tw) dl_new = add_pnvalue(dl_old) pnmean = get_pnmean(dl_new) pnmeans_list.append(pnmean) print(time.time() - start_time) ### ▲時間計測▲ ###
これを、ツイート全件履歴データフレームの右端に追加して、PN極性値でソートし、CSVで吐き出します。
# 一応、本文テキストから改行を除いておく(最初にやれ) text_list = list(tw_df['text']) for i in range(len(text_list)): text_list[i] = text_list[i].replace('\n', ' ') # ツイートID、本文、PN値を格納したデータフレームを作成 emotion_df = pd.DataFrame({'tweet_id':tw_df['tweet_id'], 'text':text_list, 'PN':pnmeans_list, }, columns=['tweet_id', 'text', 'PN'] ) # PN値の昇順でソート emotion_df = emotion_df.sort_values(by='PN', ascending=True) # CSVを出力(ExcelでみたいならUTF8ではなくShift-JISを指定すべき) emotion_df.to_csv('emotion.csv',\ index=None,\ encoding='utf-8',\ quoting=csv.QUOTE_NONNUMERIC\ )
結果をみてみる
さてどんな結果が得られたのかみてみたいと思います。
まずは、最もネガティブな方から十数件をみてみますと、
ベッキーさんの不倫事件を擁護しているツイートが最もネガティブという判定になりました。いきなり誤評価ですねw
なんでこんなことになったのか確認してみます。
>>> test_text = 'ベッキーは果たしてそんなに悪いのか' >>> dl_test = get_diclist(test_text) >>> dl_test = add_pnvalue(dl_test) >>> for w in dl_test: ... print(w) ... {'PN': 'notfound', 'POS': '一般', 'POS1': '名詞', 'BaseForm': '*', 'Surface': 'ベッキー'} {'PN': 'notfound', 'POS': '係助詞', 'POS1': '助詞', 'BaseForm': 'は', 'Surface': 'は'} {'PN': 'notfound', 'POS': '一般', 'POS1': '副詞', 'BaseForm': '果たして', 'Surface': '果たして'} {'PN': 'notfound', 'POS': '一般', 'POS1': '副詞', 'BaseForm': 'そんなに', 'Surface': 'そんなに'} {'PN': -1.0, 'POS': '自立', 'POS1': '形容詞', 'BaseForm': '悪い', 'Surface': '悪い'} {'PN': 'notfound', 'POS': '非自立', 'POS1': '名詞', 'BaseForm': 'の', 'Surface': 'の'} {'PN': 'notfound', 'POS': '副助詞/並立助詞/終助詞', 'POS1': '助詞', 'BaseForm': 'か', 'Surface': 'か'}
要するに、このツイートのなかでPN Tableに載っていた単語が「悪い」しかなく、「悪い」はPN Table上では最もネガティブな語ということになっているので、最もネガティブなツイートという判定になったわけですね。
そもそもこの文、「か」を付けた反語になっているわけですが、このように文法構造を無視して単語だけで評価すると無理があるということが、この1例からも分かります。
「良くない」とか「優れていないというわけでもない」みたいな表現を的確に評価しようと思ったら、「良い」「優れる」の部分だけみるのではなく、例えば係り受け解析というのを行って、これらの表現が打ち消されたりしていないかをきちんと調べないといけません。
係り受け解析にはCaboChaというツールがありますので、これは後日また使ってみようかと思います。
また、「オムライスなう」がなぜネガティブな評価になるのかというと、
>>> test_text = 'オムライスなう' >>> dl_test = get_diclist(test_text) >>> dl_test = add_pnvalue(dl_test) >>> for w in dl_test: ... print(w) ... {'PN': 'notfound', 'POS': '一般', 'POS1': '名詞', 'BaseForm': 'オムライス', 'Surface': 'オムライス'} {'PN': -0.9999969999999999, 'POS': '自立', 'POS1': '形容詞', 'BaseForm': 'ない', 'Surface': 'なう'}
このように、「なう」が形容詞の「ない」として判定されており、ないは否定的な言葉なので、ネガティブな評価となってしまってるわけです。
「なう」は一例ですが、要するにTwitter独自の表現を辞書に取り込まないと、適切に評価できないことが分かります。
つぎにポジティブなほうから十数件をみてみましょう。
これはネガティブ側に比べれば比較的当たってる気もしますが、「楽天ソーシャルニュースってどこが面白いんだ」のように、反語的な表現が適切に評価できていないのは先ほどみたのと同じですね。
全体として、極性値の分布がどうなっているのかをみてみます。
matplotlibでヒストグラムを描きます。
x1 = list(emotion_df['PN']) plt.hist(x1, bins=50) plt.title('P/N Frequency of My Tweets') plt.xlabel("P/N value") plt.ylabel("Frequency")
全体的に、負の値に偏っています。ゼロのところに山ができているのは、上述のとおりPN Tableに載っている単語を1語も含まないツイートが0点と評価されてるからで、これは適切な処理ではないので無視してください。
ところでこの結果が、ネガティブなツイートが多いことを意味しているのかというと、そうでもない可能性があります。というのも、PN Table自体のヒストグラムも取ってみると、
x2 = list(pn_df['PN']) plt.hist(x2, bins=50) plt.title('P/N Frequency in PN Table') plt.xlabel("P/N value") plt.ylabel("Frequency")
こんなふうになっており、そもそも大半が負の値を持つ語であるということが分かります。そういう、辞書のクセなんでしょうが、結局どのへんがニュートラルなのかはよく分かっておりません。
まとめ
今回は、負のオーラを発する自分のツイートを発掘するために、感情分析を試みました。結果的には、単にツール(MeCabや極性辞書)の使い方を学んだだけに終わり、精度的に使いものになるような処理はできてないため、「まとめて削除」まではしていません。
その主な原因は、わざわざ分析してみなくても誰でも分かる当たり前のことですが、
- 文法構造を考慮に入れていない
- Twitterの独特の表現を辞書に取り込むことができていない
といった点になると思われます。
しかしまぁ、そのあたりに課題があるということを、具体例をもって体験できたので、勉強にはなりました。今後は、処理を少しずつ改善して、納得のいく結果が出るかどうかをまた検証していきたいと思います。
最後に、感情分析に関する、参考になりそうな研究(日本語のもの)を列挙しておきます。
山本湧輝,熊本忠彦,本明代(2015).ツイートの感情の関係に基づくTwitter感情軸の決定.第7回データ工学と情報マネジメントに関するフォーラム,E5-2.
鳥倉広大,小町守,松本裕治(2012).Twitterを利用した評価極性辞書の自動拡張.言語処理学会第18回年次大会発表論文集,pp.551-554.
菅原久嗣(2010).感情語辞書を用いた日本語テキストからの感情抽出,修士論文(東京大学).
補記
あとで気づいたんですが、PN Tableには基本形だけみると同一となる語(形容詞の「ない」と助動詞の「ない」など)がいくつかあるので、基本形だけ見てPN値を取ってきているところの処理には、誤りが含まれる可能性があります。取り急ぎ修正はしてないです。以下は例です。
こういうのを考えると、上で使わなかった、形態素解析結果の品詞情報が、マッチング精度を上げるのに使えますし、さらに項目を追加して読み方の情報も取っておくべきですね。
22 助ける たすける 動詞 0.998356
487 助ける すける 動詞 0.990702
55117 ない ない 形容詞 -0.999882
55120 ない ない 助動詞 -0.999997
37424 頭 がしら 名詞 -0.466818
40768 頭 あたま 名詞 -0.513451
42098 頭 ず 名詞 -0.534602
42411 頭 つむり 名詞 -0.539412
43175 頭 かぶり 名詞 -0.551798
45300 頭 つぶり 名詞 -0.591628
50473 頭 かしら 名詞 -0.777183
52463 頭 とう 名詞 -0.980576
1781 人気 にんき 名詞 0.967650
3272 人気 じんき 名詞 0.213135
3851 人気 ひとけ 名詞 0.114632
10822 人気 ひとげ 名詞 -0.141334
2303 縁 えん 名詞 0.887527
38778 縁 ふち 名詞 -0.485352
41377 縁 へり 名詞 -0.523025
43027 縁 えにし 名詞 -0.549426
43872 縁 ゆかり 名詞 -0.564371
50448 縁 よすが 名詞 -0.775915
37424 頭 がしら 名詞 -0.466818
40768 頭 あたま 名詞 -0.513451
42098 頭 ず 名詞 -0.534602
42411 頭 つむり 名詞 -0.539412
43175 頭 かぶり 名詞 -0.551798
45300 頭 つぶり 名詞 -0.591628
50473 頭 かしら 名詞 -0.777183
52463 頭 とう 名詞 -0.980576
また、その件を掘っていて、PN Table内に不可解な情報をみつけました。
15907 ホーム ホームラン 名詞 -0.199562
19438 ホーム ホームスパン 名詞 -0.238954
21561 ホーム ホーム 名詞 -0.263255
21588 ホーム ホームドクター 名詞 -0.263565
21936 ホーム ホームステイ 名詞 -0.267942
23736 ホーム ホームドラマ 名詞 -0.289906
23854 ホーム ホームシック 名詞 -0.291620
26676 ホーム ホームグラウンド 名詞 -0.327826
28151 ホーム ホームルーム 名詞 -0.347714
28695 ホーム ホームストレッチ 名詞 -0.354835
32726 ホーム ホームヘルパー 名詞 -0.408128
11151 太刀 たちうち 名詞 -0.145596
21081 太刀 たちさばき 名詞 -0.257552
21521 太刀 たちうお 名詞 -0.262799
21820 太刀 たちすじ 名詞 -0.266431
22494 太刀 たちかぜ 名詞 -0.274693
28816 太刀 たちとり 名詞 -0.356338
33218 太刀 たち 名詞 -0.414187
36886 太刀 たちさき 名詞 -0.459479
48426 太刀 たちもち 名詞 -0.673302
1063 大人 たいじん 名詞 0.982811
2714 大人 だいにん 名詞 0.397852
3153 大人 おとなしい 形容詞 0.243448
3471 大人 おとな 名詞 0.178366
3786 大人 うし 名詞 0.124680
30217 大人 おとなびる 動詞 -0.375421
52038 大人 おとなげない 形容詞 -0.960539
2277 トップ トップ 名詞 0.911679
32105 トップ トップダウン 名詞 -0.400048
32413 トップ トップニュース 名詞 -0.404087
34165 トップ トップコート 名詞 -0.426013
37862 トップ トップマネージメント 名詞 -0.472922
1509 キング キング 名詞 0.974291
7688 キング キングメーカー 名詞 -0.092334
15140 キング キングサイズ 名詞 -0.191609
自動生成された辞書なので、こういう誤りみたいなものも含まれてるんでしょうね。
*1:読み仮名のことを「reading」としているサイトがあったのでそうしましたが自信ないです。pronounciationとかphoneticとかが正解なのかな?POSというのは品詞(part of speech)のことです。PNはポジネガの値という意味で付けました。