Python 感情極性対応表とjanomeを使って日本語で良いニュースと悪いニュースの分類を試みる

日本語のニュース文章を、感情極性対応表とjanomeを使って、良いニュース・悪いニュースで分類してみます。

livedoorニュースコーパスのロード

今回は以下で提供されているlivedoorニュースコーパスの内、トピックニュースをデータセットとして使います。

ダウンロード - 株式会社ロンウイット

ldcc-20140209.tar.gzをダウンロード・解凍すると、textディレクトリ以下に9つのディレクトリが展開されます。
livedoorトピックニュースの記事はtopic-newsディレクトリ配下にあります。 1記事1ファイルとなっており、770ファイルが収録されています。

各ファイルは、1行目に記事のURL、2行目に投稿日時、3行目に記事タイトル、4行目以降は記事本文となっており、HTMLタグなどは含まないものとなっています(以下はtext/topic-news/topic-news-5936721.txtから転載)。

http://news.livedoor.com/article/detail/5933217/
2011-10-13T11:16:00+0900
iOS5アップデートで大混乱
 日本時間13日未明(米国時間12日)、iOS4を大型アップデートしたiOS5が発表された。しかし、このiOS5にアップロード出来ない人が続出し、13日未明から大混乱が発生している。

 10月13日11時現在で、twitterのトレンドにもiOS5アップデートでエラーが出た際に表示される「Error 3200」が出現するほど騒ぎになっている。

・#iOS5 アップデートのエラー(3200,-5000,3014,復元)と対処法

 twitter上でも、エラー報告や対処法を教えるツイートで盛り上がっている。「時間をおいてやれば出来る」そうなので、しばらく待ってから更新するのがいいのかもしれない。

・iOS 5へのアップデート前に必ず準備したい4つのアドバイス

 iOS5をアップデートする助けとして、話題になっているブログでは「準備をしてからスタート」するのを勧めている。このiOS5のアップデートにまつわる混乱はすぐ収まるのだろうか。

■関連記事
・iOS5の新機能とは - アップル
・KDDIがAndroidとiPhone、Windows Phoneを扱う唯一のキャリアになるまでの戦略を振り返ってみた
・iPhone 4Sに買い替えたい6つの理由

このlivedoorトピックニュースの文章をPythonでロードします。

まずは、textディレクトリを、今回作成するPythonファイルと同じディレクトリにコピーしておきます。
次に、以下のコードでstringリスト(texts)オブジェクトにロードします。

  • 先頭2行は不要なメタ情報のため、削除しています(記事タイトルは文章の一部と捉え、残しています)
  • 文章の後ろにある"【関連記事】"や"■関連リンク"などのリンクも不要なので削除しています
import glob
import re


# livedoorトピックニュースの文章リスト
texts = []

# livedoorトピックニュースのファイル名一覧を取得する
paths = glob.glob('./text/topic-news/topic-news-*.txt')

for path in paths:
    with open(path, 'r') as f:
        original_text = f.read()
        
        # 先頭2行は不要なメタ情報のため、削除
        text = re.sub(r'^.*\n.*\n', '', original_text) 
        # "【関連"や"■関連"以降は削除
        result = re.search(r'(【|■)関連', text)
        if result is not None:
            text = text[:result.start()]
            
        texts.append(text)

# 最初の1件を表示
print(texts[0])

これで、ヘッダや関連記事へのリンクが削除されたピュアな文章がtextsにロードできます。

janomeを使った形態素解析

日本語の文章は、英語などの言語と異なり、語が区切られずに連続しているという特徴があります。 そのため、構文解析や意味解析の前に、まずは分かち書きを行う必要があります。

この分かち書き機械的に行う処理を形態素解析と言いますが、日本語の場合はMeCabJUMAN++などのエンジンを使うことで、高い精度で形態素解析ができます。

今回は、Pythonからこれら形態素解析エンジンを使うために、打田氏が開発されているjanomeというパッケージを導入します。
MeCabPythonで利用できるようになるまではMeCabのインストール、辞書のダウンロード、Pythonのラッパーの実装・組み込みなどを行う必要があって大変なのですが、janomeはコマンド一発でインストールでき、簡単にPythonから形態素解析ができるようになります(辞書もmecab-ipadic-2.7.0-20070801が内包されています)。

$ pip install janome

github.com

janome.tokenizer.Tokenizerを使うと、日本語の文章「新幹線に乗り、友人・我孫子に会いに行く。」が分かち書きされ、それぞれに品詞のラベル付けが行われていることがわかります。

from janome.tokenizer import Tokenizer


t = Tokenizer()
for token in t.tokenize('新幹線に乗り、友人・我孫子に会いに行く。'):
    print(token)
# 新幹線    名詞,一般,*,*,*,*,新幹線,シンカンセン,シンカンセン
# に  助詞,格助詞,一般,*,*,*,に,ニ,ニ
# 乗り   動詞,自立,*,*,五段・ラ行,連用形,乗る,ノリ,ノリ
# 、  記号,読点,*,*,*,*,、,、,、
# 友人   名詞,一般,*,*,*,*,友人,ユウジン,ユージン
# ・  記号,一般,*,*,*,*,・,・,・
# 我孫子    名詞,固有名詞,人名,姓,*,*,我孫子,アビコ,アビコ
# に  助詞,格助詞,一般,*,*,*,に,ニ,ニ
# 会い   動詞,自立,*,*,五段・ワ行促音便,連用形,会う,アイ,アイ
# に  助詞,格助詞,一般,*,*,*,に,ニ,ニ
# 行く   動詞,自立,*,*,五段・カ行促音便,基本形,行く,イク,イク
# 。  記号,句点,*,*,*,*,。,。,。

それでは先程ロードしたlivedoorトピックニュースの文章をjanome形態素解析します。

  • プロパティのみを持つCorpusElementというクラスを、入れ物用に作成しています
class CorpusElement:
    def __init__(self, text='', tokens=[], pn_scores=[]):
        self.text = text # テキスト本文
        self.tokens = tokens # 構文木解析されたトークンのリスト
        self.pn_scores = pn_scores # 感情極性値(後述)


# CorpusElementのリスト
naive_corpus = []

naive_tokenizer = Tokenizer()

for text in texts:
    tokens = naive_tokenizer.tokenize(text)
    element = CorpusElement(text, tokens)
    naive_corpus.append(element)

# 最初の1文章の形態素解析結果を表示
for token in naive_corpus[0].tokens:
    print(token)

先程ロードした文章の一つ(元の文章は、 http://news.livedoor.com/lite/article_detail/5903225/ )を形態素解析した結果は以下の通りになります。 固有名詞も判別できていることがわかります。

悪評   名詞,一般,*,*,*,*,悪評,アクヒョウ,アクヒョー
が 助詞,格助詞,一般,*,*,*,が,ガ,ガ
次 名詞,一般,*,*,*,*,次,ツギ,ツギ
から  助詞,格助詞,一般,*,*,*,から,カラ,カラ
次 名詞,一般,*,*,*,*,次,ツギ,ツギ
へ 助詞,格助詞,一般,*,*,*,へ,ヘ,エ
と 助詞,格助詞,引用,*,*,*,と,ト,ト
溢れ  動詞,自立,*,*,一段,連用形,溢れる,アフレ,アフレ
出る  動詞,自立,*,*,一段,基本形,出る,デル,デル
川越  名詞,固有名詞,人名,姓,*,*,川越,カワゴエ,カワゴエ
シェフ   名詞,一般,*,*,*,*,シェフ,シェフ,シェフ
・・・

感情極性対応表を使った良いニュース・悪いニュースの判定

最後にその文章が良いニュースなのか悪いニュースなのかを判定します。

単語には良いイメージ(positive)のものと悪いイメージ(negative)のものがあります。 日本語であれば、「生」「幸福」「優秀」などはポジティブ、「死」「不幸」「劣悪」などはネガティブな単語と言えるでしょう。

こうしたイメージ(極性)を数値化して単語ごとに割り当てた辞書が、東工大の奥村・高村研究室から提供されています。

PN Table

  • 「岩波国語辞書(岩波書店)」がソースとなっており、名詞49,002語、動詞4,254語、形容詞665語、副詞1,207語について数値化されています
  • 出典論文
    • 高村大也, 乾孝司, 奥村学 "スピンモデルによる単語の感情極性抽出", 情報処理学会論文誌ジャーナル, Vol.47 No.02 pp. 627--637, 2006.
    • Hiroya Takamura, Takashi Inui, Manabu Okumura, "Extracting Semantic Orientations of Words using Spin Model", In Proceedings of the 43rd Annual Meeting of the Association for Computational Linguistics (ACL2005) , pages 133--140, 2005.

pn_ja.dicをダウンロードして、上位と下位を5件ずつ見てみると、例えば「優れる」「良い」には1に近いポジティブな語、「死ぬ」「悪い」は-1に近いネガティブな語となっていることがわかります。

優れる:すぐれる:動詞:1
良い:よい:形容詞:0.999995
喜ぶ:よろこぶ:動詞:0.999979
褒める:ほめる:動詞:0.999979
めでたい:めでたい:形容詞:0.999645
・・・
ない:ない:助動詞:-0.999997
酷い:ひどい:形容詞:-0.999997
病気:びょうき:名詞:-0.999998
死ぬ:しぬ:動詞:-0.999999
悪い:わるい:形容詞:-1

ダウンロードしたpn_ja.dicをPythonファイルと同じフォルダに配置して、以下のプログラムでlivedoorトピックニュースの極性値を算出します。

  • load_pn_dict関数で、単語をキー、極性値を値とする辞書を作ります
    • pn_dic['良い'] の値は 0.999995
    • pn_ja.dicはシフトJISなので、codecsをインポートしています
  • get_pn_scores関数は、janome.tokenizer.Tokenリストから対応する極性値リストを返します
    • Token.part_of_speechで 名詞,一般,*,* などの品詞情報が取得できます(後は","で分けて0番目の要素を取得すれば、品詞がわかります)
    • また、活用形による違いを吸収して対応表にヒットさせやすいようにするため、Token.base_formプロパティで原型を取り出しています
    • pn_ja.dicは名詞、動詞、形容詞、副詞のみのため、それ以外の品詞は除外しています
import codecs


# pn_ja.dicファイルから、単語をキー、極性値を値とする辞書を得る
def load_pn_dict():
    dic = {}
    
    with codecs.open('./pn_ja.dic', 'r', 'shift_jis') as f:
        lines = f.readlines()
        
        for line in lines:
            # 各行は"良い:よい:形容詞:0.999995"
            columns = line.split(':')
            dic[columns[0]] = float(columns[3])
            
    return dic


# トークンリストから極性値リストを得る
def get_pn_scores(tokens, pn_dic):
    scores = []
    
    for surface in [t.surface for t in tokens if t.part_of_speech.split(',')[0] in ['動詞','名詞', '形容詞', '副詞']]:
        if surface in pn_dic:
            scores.append(pn_dic[surface])
        
    return scores


# 感情極性対応表のロード
pn_dic = load_pn_dict()
print(pn_dic['良い'])
# 0.999995

# 各文章の極性値リストを得る
for element in naive_corpus:
    element.pn_scores = get_pn_scores(element.tokens, pn_dic)

# 1件目の文章の極性値を表示する
print(naive_corpus[0].pn_scores)

先程の文章の極性値を表示すると、以下のように出力されます。
"悪評"は-0.994383、"次"は-0.823776、"シェフ"は"-0.0752967"、・・・と全体的にネガティブな単語が使われていることが伺えます。

[-0.994383, -0.823776, -0.823776, 0.928266, -0.0752967, -0.0125929, -0.141334, -0.585529, -0.0752967, -0.238801, -0.0856497, -0.0752967, -0.233205, -0.190665, -0.21501, 0.998699, -0.234015, -0.880775, -0.0588248, -0.288049, -0.529611, -0.637501, -0.547431, -0.312861, -0.664354, -0.0752967, -0.994383, 0.360515, -0.333928, -0.0125929, -0.595207, -0.557421, -0.496628]

極性値から文章全体のポジティブとネガティブを判定する方法します。 今回は単純に平均値を使って計算し、上位10件と下位10件を表示します。

import io


# 平均値が最も高い5件を表示
for element in sorted(naive_corpus, key=lambda e: sum(e.pn_scores)/len(e.pn_scores), reverse=True)[:10]:
    print('Average: {:.3f}'.format(sum(element.pn_scores)/len(element.pn_scores)))
    print('Title: {}'.format(io.StringIO(element.text).readline()))

# 平均値が最も低い5件を表示
for element in sorted(naive_corpus, key=lambda e: sum(e.pn_scores)/len(e.pn_scores))[:10]:
    print('Average: {:.3f}'.format(sum(element.pn_scores)/len(element.pn_scores)))
    print('Title: {}'.format(io.StringIO(element.text).readline()))

まずは平均値の高い10件の記事タイトルを見てみると、上位2件は結婚報道で納得できる感じですが、7番目は離婚報道で違和感があります。 10番目も内容はネガティブです。
また770件の記事があるにも関わらずそもそもマイナスからスタートしているのも気になります。 辞書に含まれる語の内、ポジティブな語が5122語に対して、ネガティブな語が49983語と10倍近く差があるため、ネガティブ方向に判定されやすくなっているようにも思えます。

-0.122: 結婚した亀田興毅の評価がネット掲示板で急上昇
-0.207: ゆずの北川悠仁、結婚後初のメディア出演!!大勢のリスナーからの祝福メールに大感謝!!
-0.237: 2011年・大学ミスコンの美女たちを一挙公開
-0.253: 慶應大学だけ書籍の売れ筋が「おかしい」と話題に
-0.258: 【韓国ニュース】「ワンピース」のディズニー風イラストが韓国で話題に
-0.259:  ドイツ人「日本人って“青い目で金髪”が好きなんでしょ」
-0.271: 西岡剛選手と離婚報道の徳澤直子に「笑いがとまらないだろう」
-0.275:  爆笑問題・田中裕二も驚く「ひるおび!」での恵俊彰の“天然”ぶり
-0.284: 加護亜依さんが入籍と妊娠を発表!
-0.288: 悪評が次から次へと溢れ出る川越シェフ

平均値の低い10件は以下のようになりました。 こちらは全体的に納得できる結果のようです。

-0.694: 「東電からの賠償額がわずか333円」に非難の嵐
-0.681: フジテレビの姿勢に非難殺到…番組で″火渡り″させ、老人が歩行不能の大火傷
-0.670: “金総書記死亡”に対する韓国での反応
-0.669: “すき家のバイト”が「嘔吐物を鍋に入れた」とツイートし炎上
-0.668: 渋谷駅の駅ビルで通り魔
-0.666: 大相撲で行司が土俵から転落し起き上がらず騒然
-0.664: 元光GENJI・大沢 子どもが死産するも、そのブログに「やりすぎ」の声
-0.661: 黒田勇樹のDV騒動 ネット掲示板では冷ややかな声も
-0.658: リンカーンで放送事故、ツイッター騒然
-0.654: AKB48出演の「ぷっちょ」CMに「悪寒」「気まずい」とツイッターユーザーが困惑

今回は、形態素解析で文章を構成する語を切り出し、語の持つポジティブとネガティブの極性から、文章全体の極性の判定を試みました。 結果として、ネガティブなニュースについては概ね見分けることができましたが、ポジティブなニュースについては正しく分類できていないケースもありました。

否定や反語表現にも対応するために、構文解析・意味解析も行う必要がありそうです。