SVMによる文書分類とその応用

# はじめに
ハッカソンでマッチングサービスを作ることになったときに「せっかくなら研究で学んだことを生かそう」と考え，文書分類を活用した話題提供システム(笑)みたいなものを作ったのでまとめます．
自然言語処理と機械学習の経験の少ない情弱がノリと勢いで作ったので，アプローチやプログラムが間違えてましたらコメントお願いします．


#環境
再現をされる方は以下が動く環境をご用意ください．

- python3
- mecab
- mecab-python
- gensim
- scikit-learn


#文書分類器の作成と保存
今回学習に使うのは[Livedoor News Corpus](https://www.rondhuit.com/download.html#ldcc)です．
これは各カテゴリごとにディレクトリがあって，その中に記事データが入っているので使いやすいです．
もっとちゃんとしたシステムを作ろうと思うと沢山のカテゴリと記事が必要ですが，簡単化のためにこの記事ではこれだけで学習します．

まずは各記事の文書をBoW(Bag of Words)を用いてベクトルに変換します．
BoWとは文書に単語が含まれているかのみを考えて，単語の順序は考慮しないモデルです．

ということで，まず文書から名詞のみを抽出して返す関数を用意します．

```py3:make_svm_clf.py
import MeCab

def doc2word_list(doc):
  tagger = MeCab.Tagger('-Ochasen')
  tagger.parse('')
  node = tagger.parseToNode(doc)

  word_list = []
  while node:
    meta = node.feature.split(',')
    if meta[0] == '名詞':
      word_list.append(meta[6])
    node = node.next

  return word_list
```

この関数にニュース記事を投げると，関数が記事を名詞のリストに変換します．
準備ができたのでgensimのcorporaを使って単語辞書を作って，BoWベクトルを作成します．

```py3:make_svm_clf.py
from gensim import corpora

# 名詞のリストになった記事群
documents = [...]

dic = corpora.Dictionary(documents)
# 「出現頻度が20未満の単語」と「30%以上の文書で出現する単語」を排除
dic.filter_extremes(no_below = 20, no_above = 0.3)
bow_corpus = [dic.doc2bow(d) for d in documents]
# 辞書の保存(未知文書分類のため)
dic.save_as_text('FILEPATH/livedoordic.txt')
```

次に，BoWに対してTF-IDFによる重み付けを行います．
TF(Term Frequency)はある文書中の単語の出現頻度，IDF(Inverse Document Frequency)は文書全体での単語の出現頻度を表します．
具体的な内容はggると出てきますが端的には「同じ文書に何度も出てくる単語は重要」「様々な文書に網羅的に出現する単語はあまり重要じゃない」というイメージです．
これらを掛け合わせて単語の重みとして使います．
gensimを使うとこんな感じで簡単に実装できます．

```py3:make_svm_clf.py
from gensim import models

tfidf_model = models.TfidfModel(bow_corpus)
tfidf_corpus = tfidf_model[bow_corpus]
#tf-idfモデルの保存(未知文書分類のため)
tfidf_model.save('FILEPATH/tfidf_model.model')
```

続いて，LSI(Latent Semantic Indexing)による次元圧縮を行います．
ここの話はこれを作るときに初めて触れて色々調べましたがイマイチわかりませんでした．(苦笑)
参考文献の中で書いてありましたが要するに「文章の特徴を損なわずにベクトルの次元削減を行えて，学習コストが下がる」という感じらしいです．
ということで，文書ベクトルを300次元まで圧縮します．

```py3:make_svm_clf.py
from gensim import models

lsi_model = models.LsiModel(tdidf_corpus, id2word = dic, num_topics = 300)
lsi_corpus = lsi_model[tfidf_corpus]
#lsiモデルの保存(未知文書分類のため)
lsi_model.save('FILEPATH/lsi_model.model')
```

学習の準備ができましたので，文書とカテゴリのラベルを学習させます．
分類器にはSVM(Support Vector Machine)を用います．
(SVMの話は長くなるので他にお任せしたいと思います．)
scikit-learnを使えば以下のように簡単に実装できます．

```py3:make_svm_clf.py
from sklearn import svm
from sklearn.externals import joblib

# コストパラメータやRBFカーネルパラメータはgrid_searchによって最適なパラメータを選択
svm = svm.SVC(C = 10, gamma = 0.1, kernel = 'rbf')
# train_docはベクトル化した文書のリスト
# labelsは文書のカテゴリのリスト
clf.fit(train_doc, labels)
#分類器の保存
joblib.dump(clf, 'FILEPATH/svm.pkl.cmp', compress = True)
```

これで文書分類器が完成しました．


#話題提供機能の実装
さきほど作成した文書分類器を用いてユーザにマッチング相手との話題を提供する仕組みを作ります．
といっても，普通に文書のラベルを推定させるだけなので大したことはやっていません．
今回はマッチング相手のSNSの投稿テキストがある程度手に入ったことを仮定します．

まずは，入手したSNSの投稿の全テキストを1文書として捉え，分類器のときと同様にベクトル化します．

```py3:predict_with_svm.py
from gensim import corpora
from gensim import models

dic = corpora.Dictionary.load_from_text('FILEPATH/livedoordic.txt')
# test_listはfacebookの投稿の名詞のリスト
bow = [dic.doc2bow(test_list)]
tfidf_model = models.TfidfModel.load('FILEPATH/tfidf_model.model')
tfidf = tfidf_model[bow]
lsi_model = models.LsiModel.load('FILEPATH/lsi_model.model')
lsi = lsi_model[tfidf]

```

あとは，文書分類器を読み込んで，ラベルを推定するだけです．
ラベルとカテゴリーの対応(例えばスポーツは0のようなもの)はあらかじめ準備しておきましょう．

```py3:predict_with_svm.py
from sklearn import svm
from sklearn.externals import joblib

clf = joblib.load('FILEPATH/svm.pkl.cmp')
predicted = clf.predict(test_doc)

# label_cat_dictはラベルとカテゴリのペア
print('あなたのパートナーは{0}について興味があるかもしれません．'.format(label_cat_dict[str(predicted[0])]))
```

こうすることで，SNSの投稿からマッチング相手の興味が推定でき，「あなたのパートナーはスポーツについて興味があるかもしれません．」といった出力を可能にします．


#まとめ
文書分類を活用して話題提供システムを作りました．
トピックモデルや代表語抽出の話を適用する方が実はよかった？とハッカソンの後に思ったりもしましたが，そこらへんの調査と実装はまたおいおいやる気があればやります．

#参考文献
[Bow+SVMで文書分類(1)](http://resola.ai/dev/2016/06/20/bowsvm%E3%81%A7%E6%96%87%E6%9B%B8%E5%88%86%E9%A1%9E%EF%BC%88%EF%BC%91%EF%BC%89/)