うつブログ分類器をつくってみた(結果編)

の続きです

やりたいこと

自然言語処理をつかってブログをうつ病と正常の2クラスに自動で分類したい

おおまかな手順

  1. ブログ村メンタルヘルスランキング に掲載されているブログからスクレイピング
  2. 取得したhtmlからブログ毎に名詞のみ抽出 (BoW)
  3. TfIdfなどで前処理してモデルにつっこむ
  4. 結果の解釈

2,3,4からです

名詞の抽出~前処理

文書分類というタスクを解く際に用いられる特徴量にもいろいろあると思いますが、今回はもっとも素直な方法であるBoWを行いました。

BoWとはBag of Words の略で、文章の構造を無視して単語だけに注目して文書の特徴量をつくる方法です。単語をバッグにポイポイ入れてくイメージですね(適当)
具体的には、以下の3ステップで特徴ベクトルをつくります.

  1. 文書に登場する単語を拾っていき、単語の辞書を作成する
  2. 作成した辞書と文書の単語を照らしあわして、文書毎のBoWをつくる
  3. BoWをもとに行が文書,列が単語,要素が文書内の単語出現頻度、となる行列をつくる

まずは名詞抽出部分のコードです.

import MeCab

def picking_noun(text):
    tagger = MeCab.Tagger('-Ochasen')
    tagger.parse(' ')
    node = tagger.parseToNode(text)
    noun_list=[]
    while node:
        meta = node.feature.split(',')
        if meta[0] == '名詞' and meta[6] !='*':
            noun_list.append(meta[6])
        node = node.next
    return noun_list

MeCabに文を入力すると、返り値として品詞や単語の原形を返します。
たとえば、「私は毎日6時に起きています」という文章をかませると、

BOS/EOS,*,*,*,*,*,*,*,*
名詞,代名詞,一般,*,*,*,私,ワタシ,ワタシ
助詞,係助詞,*,*,*,*,は,ハ,ワ
名詞,副詞可能,*,*,*,*,毎日,マイニチ,マイニチ
名詞,数,*,*,*,*,6,ロク,ロク
名詞,接尾,助数詞,*,*,*,時,ジ,ジ
助詞,格助詞,一般,*,*,*,に,ニ,ニ
動詞,自立,*,*,一段,連用形,起きる,オキ,オキ
助詞,接続助詞,*,*,*,*,て,テ,テ
動詞,非自立,*,*,一段,連用形,いる,イ,イ
助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
BOS/EOS,*,*,*,*,*,*,*,*

このように品詞情報がかえってくるので、これをもとに名詞のみを抽出します。
今考えてみると名詞だけじゃなくて動詞や形容詞を入れたほうがよかった気もするんですが、まあ名詞だけでも十分特徴量になります。

次に、辞書作成から頻度行列をつくる部分です. お決まりの作業ですね。

from gensim import corpora, matutils,models

def create_dense_vector(all_noun_list,no_below,no_above,num_topics):
    dic = corpora.Dictionary(all_noun_list)
    dic.filter_extremes(no_below = no_below, no_above = no_above)
    bow_corpus = [dic.doc2bow(d) for d in all_noun_list ]
    #tfidfで重み付け
    tfidf_model = models.TfidfModel(bow_corpus)
    corpus = tfidf_model[bow_corpus]
    # lsi で次元削減
    #lsi_model = models.LsiModel(corpus, id2word = dic, num_topics = num_topics)
    #corpus = lsi_model[corpus]
    dense = matutils.corpus2dense(corpus,num_terms=len(dic)).T
    return dense

ここでは、gensimで辞書作成、BoW作成、Tf-Idfによる重み付けをしています。

まず、

dic.filter_extremes(no_below = no_below, no_above = no_above)

の部分では、あまりに出現回数が少ない単語と多くの文書に共通する単語を除去しています。 たとえば、no_below = 30, no_above =0.3 とすると、
全文書で30回以下しか登場してない単語と、3割以上の文書で登場した単語を除去することができます。

Tf-Idfとは、簡単にいうと単語の頻出度に重みをつける処理です。
多くの文書で登場する単語は重要度を下げ、逆に特定の文書でしか登場しないような単語の重要度を上げるといったかんじです。詳しくはぐぐってみてください。

また、LSIでの次元削減に関してですが、
処理速度、精度の両面でそこまで変化がなかった & 1単語を1特徴量としたほうが結果の解釈がしやすい、
という理由から今回はしませんでした。

データセットについて

(ほんとはこれはじめに書いとかないといけないやつ)

さっきの作業でインプットのデータは作れたわけですが、肝心の教師ラベルをどうしようという問題があります。
正常か鬱かの二クラス分類をしたいので二種類のデータとそれに対応したラベルが必要ですが、もちろんブログの筆者が正常か鬱かなんて正確なところはわかりません。

そこで今回は便宜的に、

うつ病もしくは躁うつ病カテゴリに登録されているブログ → うつ
ブログ村総合ランキングに登録されているブログ → 正常

としてラベルを付けました。 流石にどうなのかなー、自称うつとかあやしすぎるわー
といろいろ頭をよぎりますが、 ま、気楽にいきましょう、雰囲気でいいんですよ(開き直り)

こうした結果、データ数は、

うつ 正常 合計
274 216 490

となりました。
さてさて、果たしてこれでうまく学習してくれるんでしょうか...?

結果と解釈

SVM、ナイーブベイズ、ランダムフォレストにぶちこみました(コードは省略します) 訓練データで、CV=10のクロスバリデーションでそこそこチューニングをした後、
テストデータを予測させました

その結果がこちらになります

モデル accuracy precision recall f1 AUC
SVM 0.939 0.94 0.94 0.94 0.973
NB 0.939 0.94 0.94 0.94 0.981
RF 0.929 0.94 0.93 0.93 0.954

予想以上に高い精度が出ました。
なんか怪しい感じがしますね。

そこで気になるのは、いったいどんな特徴量が重要か、ということです。
今回は次元削減などしていないので、1単語=1特徴量ということになります。
ランダムフォレストで特徴量の重要度を計算し、横軸に特徴量(単語名)、縦軸に重要度をプロットした結果が以下です。

f:id:alotofthings88:20180605160519j:plain

うつ病、うつ、症状、診断、主治医 など病気関連の単語が並んでいますね。
これを見ると、今回のデータセット内でつくったテストデータに対して高い精度が出たのは納得です。

しかしこれは当初の意図とちょっと違います。
本来は「ある人がうつ病っぽいかどうかをブログの文書で判別しよう」という趣旨なのですが、
実際は、「文書内にうつ病関連のワードが多く含まれているか」という判別を一部でしてしまっていることが分かります。

これだと、うつ病っぽい人が書いたブログにうつ病関連のことが書かれてなければ、正常クラスに誤分類してしまう可能性が高いはずです。
そのためこの92%という数字にはあまり意味がありません...
うーむ...


さて、これで終わるのもかなり残念な感じです。
せっかくなので、病気関連のワードを除去したデータに対して学習をさせてみました。

[医師,病,医者,患者,診断,主治医,症状,通院,うつ病,うつ,精神病,科]
(「科」は精神科の科だと判断 )
これらの語を除去したデータセットを作り直し、再度学習させた結果が以下です

モデル accuracy precision recall f1 AUC
SVM 0.878 0.88 0.88 0.88 0.957
NB 0.878 0.88 0.88 0.88 0.960
RF 0.867 0.88 0.87 0.87 0.952

全体的に5%近く落ちてしまいました。 とはいえある程度精度がでているので、他の特徴量もそれなりに働いているということですね。
どのようなデータが誤分類されたかというと
FP : 精神疾患以外の重度な病気(癌など)の人のブログ
FN : 昔うつだったけどいまは安定している様子のブログ
というかんじでした。

また、さきほどと同じくナイーブベイズの結果が一番良いのも少し意外ですね。
ナイーブベイズはわりかし古典的な手法なので最近はあまり使われていないイメージだったのですが、シンプルな文書分類だったら十分使えるのかもしれません。

次に特徴量の重要度グラフです(クリックで拡大できます)

f:id:alotofthings88:20180605180054p:plain

病気ワードがまだ一部見られますが、それ以外の部分で解釈をしてみると...

  • 上位に「応援」「ランキング」「クリック」「ポチ」などブログの応援を促すような名詞
  • 「睡眠」「死」「社会」「安定」などうつ病のイメージに近いワードも
  • 「リビング」「テーブル」は、うつ傾向の人はあまり外出しないので、家の中の話題が多いことを示唆している...??
  • 「抱っこ」「フード」「犬」などの謎ワード

最後のは犬を飼っていると出てきそうな単語ですよね。
うつ病であることと犬を飼っていることに相関がある...??? まあ流石に無理な解釈ですね。

まあこのように解釈が難しい部分も多いですが、とりあえず言えるのは
うつ病ブログはブログランキングの応援を促しがち」
ということですかね。
ブログを書くことを励みにしている人が多い傾向が分かったのは個人的にはまあまあ面白い発見でした

今後の課題

病気関連ワードが分類に影響をあたえてしまう問題について考えてみようと思います。
これは、主にうつ病自体を話題にしているブログをデータに採用したことが原因だと思われます。
うつ病について言及している」ということとうつ病である」ということは本来明らかに違います。 であるにもかかわらず、今回の特徴量の選び方(名詞抽出)だとそこを区別することが出来ていないんですね。

このような誤分類を避けるにはどうすればいいんでしょうか?
思いつくものとしては、

  1. 単語じゃなくて文章の構造などを特徴量にする
  2. ブログの各記事に対してトピック解析をして、トピックが病気関連の記事はデータから除外する

1はアイデアとしては良いと思うのですが、そんなに文構造に違いがあるとも思えないのでうまくいかなそう
2はわりと有りだと思います。うつ病の人の、話題が病気以外についての文書だけ集めることができるので、さきほどのような影響を避けることができそうです。

次やってみたいこと

  • ↑の2とか doc2vecを使って、うつ以外の精神疾患を含めたテキスト多クラス分類
  • Twitter API使う系のやつ
  • うつうつ言いすぎて疲れたのでもっと明るいテーマで

技術ブログはじめてで読みづらかった部分もあったと思いますが、
最後まで読んでいただきありがとうございました
これからも気分で書いていければいいなとおもいます