Python3×日本語：自然言語処理の前処理まとめ

## 初めに
####方針
・pandasは、CSVや、Mysql、SQLiteなど様々なデータベースから、取り扱いやすい自身のDataFrameに変換することができる。

・pandasのDataFrameはscikit-learnとの連携も容易である。

・自然言語処理を日本語で行う場合、適切に前処理を行わなければ、良い結果をだすことはできない。

今回は[自然言語処理における前処理の種類とその威力](https://qiita.com/Hironsan/items/2466fe0f344115aff177)を参考にさせていただき、
具体的にpandasのDataFrameの形で存在する日本語データの前処理について考えていきます。
※引用文は記載が無い場合、上記の記事からのものです。

#### 準備と想定
sqlite3からpandasのデータフレームへ変換しています。

```py3
import pandas as pd
import sqlite3

con = sqlite3.connect("db/development.sqlite3")
samples = pd.read_sql_query('select * from samples', con)
```
samplesというデータフレームの中の、pandas["comment"]に日本語データが含まれていると想定して勧めていきます。


##①テキストのクリーニング（プロジェクトの特性に合わせて行う）
(ⅰ)JavaScriptやHTMLタグの除去がよく行われる
(ⅱ)ノイズはプロジェクごとに異なるので正規表現が役に立つ

正規表現のチェック：https://regex101.com/
上記の記事の作者Hironsanさんによる例：[preprocessings/ja/cleaning.py](https://github.com/Hironsan/natural-language-preprocessings/blob/master/preprocessings/ja/cleaning.py)

例えば"山田"を、"山さん"に変換したい場合は以下のようになります。

```py3
import re
def replace_yamada(text):
    replaced_text = re.sub(r'山田', '山さん', text)
    return replaced_text


#例
text = "山田は山田らしく生きろよ！"
print(replace_yamada(text))  #=> 山さんは山さんらしく生きろよ！

#pandasaへ適用する場合
#targetという新しいカラムを作ることで元データを汚さないようにしています
samples["target"] = samples["comment"].apply(replace_yamada)
```

##②単語の分割
日本語は、品詞ごとにスペースを開けて並んでいるわけではないので、とりあつかいしやすいようにスペースを開けて配置します。
今回は、Mecab + mecab-ipadic-NEologd(Mecabの辞書) + mecab-python3(Mecabとのバインディング)を利用しています。
セットアップはこちらを参照ください：[Mecabセットアップ for Ubuntu](https://qiita.com/chamao/private/23cd938cdcd6773d54d2)

```py3
import MeCab
def wakati_by_mecab(text):
    tagger = MeCab.Tagger('')
    tagger.parse('') 
    node = tagger.parseToNode(text)
    word_list = []
    while node:
        pos = node.feature.split(",")[0]
        if pos in ["名詞", "動詞", "形容詞"]:   # 対象とする品詞
            word = node.surface
            word_list.append(word)
        node = node.next
    return " ".join(word_list)

#例
text = 'どれだけの天才でもどれだけの馬鹿でも自分一人だけの純粋な世界なんて存在しえないんだ。'
print(wakati(text))  #=> どれ 天才 どれ 馬鹿 自分 一 人 純粋 世界 存在 し え ん

samples["target"] = samples["target"].apply(wakati)
```

[【特別連載】 さぁ、自然言語処理を始めよう！（第2回： 単純集計によるテキストマイニング）](https://datumstudio.jp/blog/%E3%80%90%E7%89%B9%E5%88%A5%E9%80%A3%E8%BC%89%E3%80%91-%E3%81%95%E3%81%81%E3%80%81%E8%87%AA%E7%84%B6%E8%A8%80%E8%AA%9E%E5%87%A6%E7%90%86%E3%82%92%E5%A7%8B%E3%82%81%E3%82%88%E3%81%86%EF%BC%81-2)


※jaumanpp使う場合
[JUMAN++ × Python3 × Ubuntu17](https://qiita.com/chamao/private/109cfb97f67899f5dcd1)

```py3
from pyknp import Jumanpp

def wakati_by_jumanpp(text):
    jumanpp = Jumanpp()
    result = jumanpp.analysis(text)
    word_list = []
    for mrph in result.mrph_list():
        hinsi = mrph.hinsi
        if hinsi in  ["名詞", "動詞", "形容詞"]:  # 対象とする品詞
            word = mrph.midasi
            word_list.append(word)
    return " ".join(word_list)
```



##③単語の正規化
コード参照・引用：[preprocessings/ja/normalization.py](https://github.com/Hironsan/natural-language-preprocessings/blob/master/preprocessings/ja/normalization.py)

###③−１：文字種の統一
>文字種の統一ではアルファベットの大文字を小文字に変換する、半角文字を全角文字に変換するといった処理を行います。たとえば「Natural」の大文字部分を小文字に変換して「natural」にしたり、「ﾈｺ」を全角に変換して「ネコ」にします。このような処理をすることで、単語を文字種の区別なく同一の単語として扱えるようになります。

######③-1'アルファベットの大文字を小文字に変換（必須）
```py3
def lower_text(text):
    return text.lower()

#例
text = "NatuRAL"
print(lower_text(text))  #=> natural

#pandasへ適用
samples["target"] = samples["target"].apply(lower_text)
```

######③-1''：''半角文字を全角文字に変換（必須）
mojimojiというライブラリを使います。

```bash:terminal
$  pip install mojimoji
```

```py3
import mojimoji

#例
print(mojimoji.zen_to_han('私アイyabc012')) #=> '私ｱｲyabc012'

#pandasへ適用
samples["target"] = samples["target"].apply(mojimoji.zen_to_han)
```

[Pythonで半角・全角の変換を高速に行う](https://qiita.com/ikuyamada/items/fea6c8f81e7cac7cf318)
>同じくPythonで半角・全角を変換するライブラリzenhan及びjctconvと動作速度を比較してみます。
Pure Pythonで実装されたzenhanライブラリと比較して約18倍、jctconvと比較して約5倍、高速に処理できていることが分かります。


###③−２数字の置き換え（要考察）
>数字の置き換えではテキスト中に出現する数字を別の記号(たとえば0)に置き換えます。
数字の置き換えを行う理由は、数値表現が多様で出現頻度が高い割には自然言語処理のタスクに役に立たないことが多いからです。

```py3
def normalize_number(text):
    # 連続した数字を0で置換
    replaced_text = re.sub(r'\d+', '0', text)
    return replaced_text

#例
text = "江頭2:50はいつも元気である"
print(normalize_number(text)) #=> 江頭0:0はいつも元気である
```

上記の例のように重要な意味を持つ単語も変換してしまう可能性があるので工夫が必要です。

分かち書きをした後の要素に対して行い、ある一定の条件のみに数字を０に置き換えるというように、
プロジェクトごとに柔軟に対処しなければいけないかもしれません。

以下の例では、++++円、++++度といった言葉をそれぞれ、0円、0度に変換しています。

```py3
def  normalize_number_for_wakati(wakati_text):
    wakati_list = wakati_text.split(" ")
    for i,w in enumerate(wakati_list):
        petterns = [ r"\d+円", r"\d+度"]    
        for pettern in petterns:
            if re.match(pettern , w):
                w = re.sub(r'\d+', '0', w)
        wakati_list[i] = w
    replaced_wakati_text = (" ").join(wakati_list)
    return replaced_wakati_text

text = "江頭2:50は40度の高熱でも元気である。ギャラも500円でも気にしない。"
wakati_text = wakati(text)
wakati_text # => '江頭2:50 40度 高熱 元気 ギャラ 500円 気 し'
normalize_number_for_wakati(wakati_text) #=> '江頭2:50 0度 高熱 元気 ギャラ 0円 気 し'
```

※江頭2:50を固有名刺として分離できるかは、Mecabが使用している辞書に依存しています（今回は mecab-ipadic-NEologdを使用しています）。

###③−３：辞書を用いた単語の統一
例えば、以下のような単語を１種類とみなしたい。
・文字種の多様性：リンゴ、リンゴ、林檎
・外来語表記の異なり：コンピュータ、コンピューター

この問題は日本語において特に根が深く、未だに根本的な解決がされていないようです。
Pythonでの対処方法として代表的なものをあげます（詳しくは下記参照）。

①from gensim.models import word2vec
[Word2Vecを用いた類義語の抽出が上手く行ったので、分析をまとめてみた](https://qiita.com/To_Murakami/items/cc225e7c9cd9c0ab641e)

②from nltk.corpus import wordnet
[Python による日本語自然言語処理](http://www.nltk.org/book-jp/ch12.html#wordnet)

”表記ゆれ”に関する参照
[雪だるまプロジェクト](http://snowman.jnlp.org/snowman)
[日本語の表記ゆれ問題に関する考察と対処](http://www.japio.or.jp/00yearbook/files/2015book/15_2_07.pdf)


##④ストップワードの除去(必須)
>ストップワードというのは自然言語処理する際に一般的で役に立たない等の理由で処理対象外とする単語のことです。

###④−１：辞書による方式
sklearnのCountVectorizerを単語の数え上げに使うのならば、stop_wordsをオプションで指定することができます。
オプションのstop_wordsはlistなので、以下ではstop_wordsのlistを作成しています。

```py3
import os
import urllib.request

def download_stopwords(path):
    url = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
    if os.path.exists(path):
        print('File already exists.')
    else:
        print('Downloading...')
        # Download the file from `url` and save it locally under `file_name`:
        urllib.request.urlretrieve(url, path)
        
def create_stopwords(file_path):
    stop_words = []
    for w in open(path, "r"):
        w = w.replace('\n','')
        if len(w) > 0:
          stop_words.append(w)
    return stop_words    

path = "stop_words.txt"
download_stopwords(path)
stop_words = create_stopwords(path)
```

こんな感じのデータフレームで試します。

```py3
comments = ["あなたは春が好きね","夏はあいつの季節さ"]
sample_dict = {"comment": comments} 
samples = pd.DataFrame.from_dict(sample_dict)
samples["target"] = samples["comment"].apply(wakati)
```

・sotp_wordsを指定しない場合

```py3
from sklearn.feature_extraction.text import CountVectorizer
count_vectorizer = CountVectorizer(token_pattern=u'(?u)\\b\\w+\\b')
feature_vectors = count_vectorizer.fit_transform(samples['target'])
vocabulary = count_vectorizer.get_feature_names() #=> ['あいつの季節', 'あなた', '夏', '好き', '春']
```
（”あいつの季節”は１語にカウントされるとは思いませんでしたが。。。。）

・sotp_wordsを指定

```py3
count_vectorizer = CountVectorizer(stop_words=stop_words,token_pattern=u'(?u)\\b\\w+\\b')
feature_vectors = count_vectorizer.fit_transform(samples['target'])
vocabulary = count_vectorizer.get_feature_names() #=> ['あいつの季節', '好き']
```
stop_wordsに含まれている、春、夏、あなたが含まれていないことがわかります。


###④−２：出現頻度による方式
④−１の方式と併用するのもありだと思います。
tf-idfが一番有名だと思います。

[TF-IDF で文書をベクトル化。python の TfidfVectorizer を使ってみる](http://ailaby.com/tfidf/)
>特定の文書にだけ現れる単語と、ありふれた単語に差をつけます。つまり、各単語の希少性を考慮にいれることを考えます。
そこで登場するのが TF-IDF です。

TfidfTransformerを使うことで簡単に利用できます。
[scikit-learnでtf-idf](http://otknoy.hatenablog.com/entry/2015/10/11/200650)

```py3
from sklearn.feature_extraction.text import TfidfTransformer
tfidf_transformer = TfidfTransformer(norm='l2', sublinear_tf=True)
tfidf = tfidf_transformer.fit_transform(feature_vectors)
```


##参照
[Pythonでの正規表現の使い方](https://qiita.com/wanwanland/items/ce272419dde2f95cdabc)
[sklearn.feature_extraction.text.CountVectorizer](http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html)