Python3を使った日本語自然言語処理(4)ロジスティック回帰による感情分析

##TF-IDFによる単語の関連性の評価とは
前回までで文章をパースし、単語を特徴ベクトルに変換することを行いました。ただ、ある単語がたくさん文章中で存在していても、それがどのカテゴリーの文章でもたくさん登場する単語であれば、カテゴリーを判断する上でその単語の重要性はあまり高くはありません。
ある映画レビューを「肯定的なもの」「否定的なもの」で分類したい時、「すごい」という単語は『すごいつまらなかった』という文脈でも『すごいよかった』という文脈でも頻繁に使われうるので、これだけではそのレビューのネガポジは判断するのが難しいです。
こういった感じで、ある単語がカテゴリーわけを行う際、重要であればその単語の重みをあげ、重要でなければ下げる手法が「TF-IDF」です。TFは単語の出現頻度を、IDFは逆文書頻度と呼ばれ、定義は以下のようになります。
$n_d$はドキュメントの総数、$df(t, d)$は単語tを含んでいるドキュメントの個数を表すとすると、
$idf(t, d) = log \frac{n_d}{1 + df(t,d)}$, 
$tf-idf = tf(t,d) \times idf(t, d)$ 

ちなみにこれを比較的簡単に実装できるのがPythonのscikit-learnにあるTfidfTransformerクラスであり、これは前回使用したCountVectorizerから単語の出現頻度を受け取り、変換してくれます。

```tf_idf.py
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer 
from sklearn.feature_extraction.text import TfidfTransformer

count = CountVectorizer()
texts = np.array(["He likes to play the guitar", \
"She likes to play the piano", \
"He likes to play the guitar, and she likes to play the piano"])
bag = count.fit_transform(docs)

tfidf = TfidfTransformer(use_idf=True, norm='l2', smooth_idf=True)
np.set_printoptions(precision=2)
print(tfidf.fit_transform(count.fit_transform(docs)).toarray())

出力結果:
[[ 0.    0.48  0.48  0.37  0.    0.37  0.    0.37  0.37]
 [ 0.    0.    0.    0.37  0.48  0.37  0.48  0.37  0.37]
 [ 0.34  0.26  0.26  0.4   0.26  0.4   0.26  0.4   0.4 ]]

```

##実際に文章データを解析する場合の留意点
###テキストデータのクレンジング
上の例のような文章では、入力に余分な記号などは含まれず、そのままcountVectorizerなどに渡すことができています。ただテキストデータの中にはhtmlマークアップを含むものや区切り線などが含まれたりして、解析を始める前にこういった余計なデータを除く必要があります（テキストデータのクレンジング）。これはPythonの正規表現などで行うことが可能です。
[Pythonにおける正規表現操作](https://docs.python.jp/3/library/re.html)

###ストップワーズの除去
文章のカテゴリなどによらず、一般的にある言語において文中によく出現する単語はあまり文章の分類に重要度が高くないため、機械学習を実際に行うより前にのぞいてしまうのがベター（らしい。）
英語ではPythonのNLTKライブラリからstopwordsを持ってこられるが、日本語では公式のライブラリは存在せず、[slothlib](http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt)のページを読み込み、ソースを解析して単語をとってくることが多いらしいです。こんな感じのコードでまずurlを開き、そこからソースをBeautifulSoupという謎なものを使って検証します。

```ja_stopwords.py
import urllib.request
import bs4
from bs4 import BeautifulSoup

def get_stop_words():
    #stopwords(文章の属性に関わらず頻出する単語)を除外するために、slothlibの日本語ようストップワードリストを取得。
    #urllibとBeautifulSoupによりソースをパース。
    url = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
    soup = bs4.BeautifulSoup(urllib.request.urlopen(url).read(), "html.parser")
    ss = str(soup)
    return ss

print(get_stop_words())

出力結果:
あそこ
あたり
あちら
あっち
あと
あな
あなた
あれ
いくつ
いつ
いま
い
...

```

##ドキュメントを分類するロジスティック回帰モデルのトレーニング
じゃあこんな感じで特徴量など求め、前処理を行った文章に対して実際にロジスティック回帰分析を行い、機械学習である文章がポジティブなものかネガティブなものかを分類していきます。日本語で手頃なソースがなかった（Twitterからの収集が多いみたいですがそのためにAWSとかいじるのめんどいので）
英語の映画レビューに対してネガティブかポジティブかアノテーションされたデータをもとにやってみる。
このプログラムは[Python Machine Learning](https://www.amazon.co.jp/Python%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E9%81%94%E4%BA%BA%E3%83%87%E3%83%BC%E3%82%BF%E3%82%B5%E3%82%A4%E3%82%A8%E3%83%B3%E3%83%86%E3%82%A3%E3%82%B9%E3%83%88%E3%81%AB%E3%82%88%E3%82%8B%E7%90%86%E8%AB%96%E3%81%A8%E5%AE%9F%E8%B7%B5-impress-top-gear/dp/4844380605/ref=sr_1_cc_2?s=aps&ie=UTF8&qid=1487516026&sr=1-2-catcorr&keywords=python+%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92)という本の自然言語処理の章を参考にしました。

```reviews.py
from sklearn.pipeline import Pipeline 
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import GridSearchCV

X_train = df.loc[:25000, 'review'].values
y_train = df.loc[:25000, 'sentiment'].values
X_test = df.loc[25000:, 'review'].values
y_test = df.loc[25000:, 'sentiment'].values

tfidf = TfidfVectorizer(strip_accents=None, lowercase=False, preprocessor=None)

param_grid = [{'vect__ngram_range': [(1, 1)],
               'vect__stop_words': [stop, None],
               'vect__tokenizer': [tokenizer, tokenizer_porter],
               'clf__penalty': ['l1', 'l2'],
               'clf__C': [1.0, 10.0, 100.0]},
              {'vect__ngram_range': [(1, 1)],
               'vect__stop_words': [stop, None],
               'vect__tokenizer': [tokenizer, tokenizer_porter],
               'vect__use_idf':[False],
               'vect__norm':[None],
               'clf__penalty': ['l1', 'l2'],
               'clf__C': [1.0, 10.0, 100.0]},
              ]

                     ('clf', LogisticRegression(random_state=0))])

gs_lr_tfidf = GridSearchCV(lr_tfidf, param_grid,
                           scoring='accuracy',
                           cv=5,
                           verbose=1,
                           n_jobs=-1)
gs_lr_tfidf.fit(X_train, y_train)
print('Accuracy: %.3f' % gs_lr_tfidf.best_score_)

```
一番最初にdfをトレーニンングデータとテストデータに分割する。
dfはpandasライブラリの中のデータフレームで表みたいになっているとイメージしてみるとわかりやすいかも。
![image](https://qiita-image-store.s3.amazonaws.com/0/147809/d84900fe-e7c6-501b-4940-ca5939a3b2b1.png)

この中の['review']['sentiment']にそれぞれレビューの文章（これがxとなる）、そのレビューに対するラベル（0,1でポジティブかネガティブか表す）であり、これがyとなる。
（dfが実際にどう元データを読み込んだか、データのクレンジングを行ったかは割愛…）
そのあとにGridSearchCVというsklearnのクラスを使ってロジスティック回きのための最適なパラメータをチューニングしています。gs_lr_tfidfというGridSearchCVのインスタンスを生成し、それをX_train, y_trainを使ってgs_lr_tfidf.fit()でトレーニングを行っています。

（[sklearnでハイパーパラメータのチューニング](http://qiita.com/sotetsuk/items/16ffd76978085bfd7628)）

ただこのやり方を実際に行うと凄まじく時間がかかります…
のでデータが大きい時はアウトオブコア学習なるものを行うのが一般的らしい。