TF-IDF変換を保存する

# 前置き

TF-IDF変換を未知の入力に対して使うため、変換した内容を保存する必要があるため、今回はその保存方法について残しておく。

# mecabで形態素解析
[tyamagu2.xyzさんの記事](http://tyamagu2.xyz/articles/ja_text_classification/)を引用してクラス定義してみます。
※`MeCab`をimport忘れずに
※ストップワードは、京大の[Slothlib](http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt)を使っているので、引っ張って来てください

```python3
import MeCab
'''
MeCabで名詞だけを抜き取るクラスの定義
辞書は"mecab-ipadic-neologd"を使用
'''


class WordDividor:
    INDEX_CATEGORY = 0
    TARGET_CATEGORY = ['名詞']

    def __init__(self, dictionary="mecabrc"):
        self.dictionary = dictionary
        self.tagger = MeCab.Tagger(self.dictionary)

    def extract_words(self, text):
        if not text:
            return []

        # テキストの余分なデータの削除(ex : httpなど)
        text = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", text)
        text = re.sub(r'[!-~]', "", text)  # 半角記号,数字,英字
        text = re.sub(r'[︰-＠]', "", text)  # 全角記号
        text = re.sub('\n', " ", text)  # 改行文字

        words = []
        # 文字列がGCされるのを防ぐ
        self.tagger.parse('')
        node = self.tagger.parseToNode(text)
        while node:
            # "，"で文字列の分割を行い, ターゲットになる品詞と比較を行う.
            if node.feature.split(',')[self.INDEX_CATEGORY] in self.TARGET_CATEGORY:
                # ストップワードの判定を行う(stopwordsに引っかかってない名詞を入れる)
                if node.surface not in stopwords:
                    words.append(node.surface)
            node = node.next

        return words

```

使う時は、

```python3
wd = WordDividor()
extract_word = wd.extract_word(text="ほげほげテスト")
```
このように使うと、一時的に用意した変数`extract_word`にmecabにより形態素解析され、名詞だけ抜き出されたものが入ります。


# TF-IDF変換
次に、[TF-IDF変換](http://gri.jp/report/data-science/10011)を行います。
これも下記のようにクラス定義してあげます.
※`TfidfVectorizer`をimport忘れずに

```python3
from sklearn.feature_extraction.text import TfidfVectorizer

'''
TF-IDF変換を行うクラスの定義
1. TfidfVectorizerインスタンスの作成(analyzerにmecabの形態素解析手法を指定)
2. fitメソッドを行ったTfidfVectorizerインスタンスを返す(新しいデータに用いるため)
'''


class WordPreProcessor:

    # コンストラクタ
    def __init__(self, analyze_method):
        self.tfidf = TfidfVectorizer(analyzer=analyze_method, use_idf=True, norm='l2', smooth_idf=True)
   
    def wordspreprocess(self, raw_data):
        # fitの状態を格納(TfidfVectorizerインスタンスが格納される)
        tfidf_condition = self.tfidf.fit(raw_data)
        # 形態素解析されたワードを、fitで作ったTF-IDF特徴空間行列に変換する
        tfidf_vector = self.tfidf.transform(raw_data).toarray()
   
        return tfidf_condition, np.matrix(tfidf_vector)
```
ここで2つpointがあり、

- *[TfidfVectorizerインスタンスにMeCabで形態素解析するクラス・メソッド名を指定してあげる。](http://tyamagu2.xyz/articles/ja_text_classification/)*

英語などを形態素解析する場合はそもそもMeCabが必要ではないので、[python機械学習プログラミング](http://amzn.asia/8Zf3cZ6)なんかは
`TfidfVectorizer(analyzer='word', use_idf=True, norm='l2', smooth_idf=True)`
と[デフォルト指定](http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html)で生成しています。

なのでインスタンス生成する際は

```python3
wpp = WordPreProcessor(analyze_method=wd.extract_words)
```
こんな感じで、wdインスタンスのメソッドを指定してあげます。

- *fit_transformを使わずfitだけ使う*

fitメソッドは[公式ライブラリ](http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html)を引用すると

>Learn vocabulary and idf from training set.

訓練データからidfと語彙を学習します。返り値は学習後のTfidfVectorizerインスタンスです。
そのインスタンスを使ってTF-IDF変換を行うメソッドが`transform`メソッドになります。
公式のライブラリではこの２つを一緒くたにした`fit_transform`メソッドが親切に用意されているので、用途で使い分けるべきだと思います。

ということで、fitした状態をpickleなどで保存すれば、新しい文章を保存した規準でTF−IDF変換できるということです。


# TF-IDF保存

```python3
# オブジェクト保存ライブラリの導入 
import pickle 
# インスタンスの生成 
wpp = WordPreProcessor(analyze_method=wd.extract_words)
tfidf_condition, feature_matrix = wpp.wordspreprocess(raw_data="ほげほげテスト", is_dimension_compact=is_dimension_compact)
# 保存 
pickle.dump(tfidf_condition, open("tfidf.pkl", "wb"))
```
しかしながらこれだとエラーになります。
<img width="1255" alt="スクリーンショット 2018-01-30 12.33.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/211139/4e6a80c3-8ef1-61dd-435d-c38444df5f24.png">

SwigPyObject？？？？

どうやら、analyze_methodに形態素クラスのメソッドを指定しているのでクラスインスタンスのネストになっているため保存できないということなのだ。

なので、この解決策は

```
tfidf_condition.set_params(analyzer='word')
# 保存 
pickle.dump(tfidf_condition, open("tfidf.pkl", "wb"))
```
一旦analyzer='word'と置き換えることで保存する('word'はデフォルトである)

使う際は,

```python3
from dataPreProcessor import WordDividor
import pickle

def main():
    wd = WordDividor()
    tfidf_vectorizer = pickle.load(open("tfidf.pkl", "rb"))
    # 一時的に置き換えていたanalyzerを指定
    tfidf_vectorizer.set_params(analyzer=wd.extract_words)
    tfidf_vector = tfidf_vectorizer.transform(['新しくものあるほげほげテスト'])

if __name__ == '__main__':
    main()
```
こんな感じで、保存したものをロードして、`set_params`メソッドを使って再度MeCabクラスのメソッドを指定し直して、使ってあげる。
fitで学習済みなので、analyzeメソッドが一時的に変更されても問題ありません。

# 最後に
意外と回りくどいやり方をしているので、そもそも`pickleがナンセンス`とか他にいい方法があればコメントください。

# 参考

- [MeCab と scikit-learn で日本語テキストを分類する](http://tyamagu2.xyz/articles/ja_text_classification/)

- [Keep TFIDF result for predicting new content using Scikit for Python
(これの回答4)](https://stackoverflow.com/questions/29788047/keep-tfidf-result-for-predicting-new-content-using-scikit-for-python)