自然言語処理の分類タスクを試行錯誤する

[自然言語処理 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/nlp)の23日目の記事です。

## 自然言語処理で必要なフロー
* データを集める
* 前処理 (x)
* アルゴリズム適用 (x)
* 過、未学習への対策
* パラメーターチューニング (x)
* モデルの評価

機械学習の一般的なフローは上記で今回は色んなパターンでxの部分を試行錯誤したいと思います。
タスクはLivedoorが提供しているニュースの文章とそのカテゴリを分類する機械学習プログラムを作ります。

## github
https://github.com/akichim21/natural-language-classifier

## 初期バージョンのコード

```python:01_janome_bow_lr.py
# This Python file uses the following encoding: utf-8
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from preprocessings.tokenizer import JanomeTokenizer
from preprocessings.normalizer import normalize
from preprocessings.livedoor import load_df

# janomeでトークナイズ
# BOWで文をベクトル化
# LogisticRegressionで分類

# janomeでトークナイズ
janome = JanomeTokenizer()
def tokenize(word):
    tokens = [normalize(janome.surface(token)) for token in janome.tokenize(word)]
    return " ".join(tokens)

# livedoorの記事をラベル付きでDataFrameとして読み込み
df = load_df()

# 文全てをtokenize
df['docs'] = df['docs'].apply(tokenize)

# BOWで文をベクトル化
count = CountVectorizer()
X_count = count.fit_transform(df['docs'].values)

# トレーニング:テスト = 8:2で分ける
X_train, X_test, Y_train, Y_test = train_test_split(X_count, df['labels'], test_size=0.2,random_state=3)

# LogisticRegressionで分類
clf = LogisticRegression()
clf.fit(X_train, Y_train)

# テストデータで正確性(accuracy)を表示
print(clf.score(X_test, Y_test))
```

## 解説

```
df = load_df()
```

* データを集める
  * 今回はlivedoorの記事の文章とカテゴリを使う

```
# janomeでトークナイズ
janome = JanomeTokenizer()
def tokenize(word):
    tokens = [normalize(janome.surface(token)) for token in janome.tokenize(word)]
    return " ".join(tokens)


# 文全てをtokenize
df['docs'] = df['docs'].apply(tokenize)

# BOWで文をベクトル化
count = CountVectorizer()
X_count = count.fit_transform(df['docs'].values)
```
* 前処理
  * 文章の特徴量を取り出したいので、単語に分割してベクトル化(コンピュータが扱える数値化)する
  * 単語分割でjanome, ベクトル化でBag Of Wordを使う

```
# LogisticRegressionで分類
clf = LogisticRegression()
clf.fit(X_train, Y_train)
```

* アルゴリズム適用
  * X_train: 文章をベクトル化した行列
  * Y_train: categoryの数値
  * 今回はLogisticRegressionを使う

```
# トレーニング:テスト = 8:2で分ける
X_train, X_test, Y_train, Y_test = train_test_split(X_count, df['labels'], test_size=0.2,random_state=3)

# テストデータで正確性(accuracy)を表示
print(clf.score(X_test, Y_test))
```

* モデルの評価
  * 全体のデータをテストデータとトレーニングデータに分ける
  * accuracyで性能評価


## 前処理を試行錯誤

```
mecab = MeCabTokenizer()
def tokenize(word):
    tokens = [mecab.surface(token) for token in mecab.tokenize(word)]
    return " ".join(tokens)
```

形態素解析をjanomeからmecabに変えてみる

```
# TFIDFで文をベクトル化
count = CountVectorizer()
X_count = count.fit_transform(df['docs'].values)
tfidf_transformer = TfidfTransformer()
X_tfidf = tfidf_transformer.fit_transform(X_count)
```

ベクトル化をTFIDFにしてみる

さらに知りたい

**形態素解析**

* 単語
  * [新語・固有表現に強い「mecab-ipadic-NEologd」の効果を調べてみた](https://engineering.linecorp.com/ja/blog/detail/109)
* 構文解析
  * [Pythonの字句/構文解析ライブラリ](https://qiita.com/shinsa82/items/94d800df630e63511014)
* 意味解析
  * [【Python】自然言語処理でラーメン屋を分類してみる](https://qiita.com/naotaka1128/items/87d717961bd0c34e7a64)

**ベクトル化**

* [Doc2Vecの仕組みとgensimを使った文書類似度算出チュートリアル](https://deepage.net/machine_learning/2017/01/08/doc2vec.html)
* [Word2Vecを用いた類義語の抽出が上手く行ったので、分析をまとめてみた](https://qiita.com/To_Murakami/items/cc225e7c9cd9c0ab641e)

## アルゴリズムを試行錯誤

```
# MultinomialNBで分類
nb = MultinomialNB()
nb.fit(X_train, Y_train)
```

```
# SVMで分類
svm = SVC()
svm.fit(X_train, Y_train)
```

```
# LogisticRegressionで分類
clf = LogisticRegression()
clf.fit(X_train, Y_train)
```

さらに知りたい

* 分類
 * [DeepLearningで食事画像のクラス分類器を作成してみた](http://developer-blog.finc.co.jp/post/137854168027/deeplearning%E3%81%A7%E9%A3%9F%E4%BA%8B%E7%94%BB%E5%83%8F%E3%81%AE%E3%82%AF%E3%83%A9%E3%82%B9%E5%88%86%E9%A1%9E%E5%99%A8%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F)

* アルゴリズム全般 
  * [代表的な機械学習手法一覧](https://qiita.com/tomomoto/items/b3fd1ec7f9b68ab6dfe2)

##　パラメータチューニング

```
grid_list = [
    # CountVectorizer, LogisticRegressionを検証
    # CountVectorizer: tokenizer
    # Logisticregression: penalty, C
    {
        'param_grid': {
            'vect__ngram_range': [(1, 1)],
            'vect__tokenizer': [tokenize, tokenize_by_pos],
            'clf__penalty': ['l1', 'l2'],
            'clf__C': [1.0, 10.0, 50.0, 100.0]
        },
        'pipeline': Pipeline([
            ('vect', CountVectorizer()),
            ('clf', LogisticRegression())
        ])
    },
    # TfidfVectorizer, LogisticRegressionを検証
    # TfidfVectorizer: tokenizer, use_idf
    # Logisticregression: penalty, C
    {
        'param_grid': {
            'vect__ngram_range': [(1, 1)],
            'vect__tokenizer': [tokenize, tokenize_by_pos],
            'vect__use_idf': [True, False],
            'clf__penalty': ['l1', 'l2'],
            'clf__C': [1.0, 10.0, 50.0, 100.0]
        },
        'pipeline': Pipeline([
            ('vect', TfidfVectorizer()),
            ('clf', LogisticRegression())
        ])
    },
]
for grid in grid_list:
    gs = GridSearchCV(grid["pipeline"], grid["param_grid"], n_jobs = -1, cv = 10, scoring = 'accuracy', verbose = 1)
    gs.fit(X_train, Y_train)
    print('Best parameter set: %s' % gs.best_params_)
    print('CV Accuracy: %.3f' % gs.best_score_)
    clf = gs.best_estimator_
    print('Test CV Accuracy: %.3f' % clf.score(X_test, Y_test))
```

アルゴリズム、ベクトル化、パラメータを複数実行して評価し、一番良いものを採用する

## まとめ
自然言語処理の概要と試行錯誤の例をあげてみました。
精度が上がらないときはどこがボトルネックか、どういう手法が他にあるかを把握しながらやるとうねるかと思います。

## Appendix
[Scikit learnで学ぶ機械学習入門](https://www.slideshare.net/tkm2261/scikit-learn)
[自然言語処理における前処理の種類とその威力](https://qiita.com/Hironsan/items/2466fe0f344115aff177)
[Python機械学習プログラミング 達人データサイエンティストによる理論と実践](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_20?ie=UTF8&qid=1513943478&sr=8-20&keywords=%E6%A9%9F%E6%A2%B0+%E5%AD%A6%E7%BF%92)