機械学習
pandas
Python3

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

初めに

方針

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

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

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

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

準備と想定

sqlite3からpandasのデータフレームへ変換しています。

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

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

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

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回: 単純集計によるテキストマイニング)

※jaumanpp使う場合
JUMAN++ × Python3 × Ubuntu17

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

③−1:文字種の統一

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

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

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

#pandasへ適用
samples["target"] = samples["target"].apply(lower_text)
③-1'':''半角文字を全角文字に変換(必須)

mojimojiというライブラリを使います。

terminal
$  pip install mojimoji
import mojimoji

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

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

Pythonで半角・全角の変換を高速に行う

同じくPythonで半角・全角を変換するライブラリzenhan及びjctconvと動作速度を比較してみます。
Pure Pythonで実装されたzenhanライブラリと比較して約18倍、jctconvと比較して約5倍、高速に処理できていることが分かります。

③−2数字の置き換え(要考察)

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

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円、0度に変換しています。

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を使用しています)。

③−3:辞書を用いた単語の統一

例えば、以下のような単語を1種類とみなしたい。
・文字種の多様性:リンゴ、リンゴ、林檎
・外来語表記の異なり:コンピュータ、コンピューター

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

①from gensim.models import word2vec
Word2Vecを用いた類義語の抽出が上手く行ったので、分析をまとめてみた

②from nltk.corpus import wordnet
Python による日本語自然言語処理

”表記ゆれ”に関する参照
雪だるまプロジェクト
日本語の表記ゆれ問題に関する考察と対処

④ストップワードの除去(必須)

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

④−1:辞書による方式

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

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)

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

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

・sotp_wordsを指定しない場合

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() #=> ['あいつの季節', 'あなた', '夏', '好き', '春']

(”あいつの季節”は1語にカウントされるとは思いませんでしたが。。。。)

・sotp_wordsを指定

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に含まれている、春、夏、あなたが含まれていないことがわかります。

④−2:出現頻度による方式

④−1の方式と併用するのもありだと思います。
tf-idfが一番有名だと思います。

TF-IDF で文書をベクトル化。python の TfidfVectorizer を使ってみる

特定の文書にだけ現れる単語と、ありふれた単語に差をつけます。つまり、各単語の希少性を考慮にいれることを考えます。
そこで登場するのが TF-IDF です。

TfidfTransformerを使うことで簡単に利用できます。
scikit-learnでtf-idf

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

参照

Pythonでの正規表現の使い方
sklearn.feature_extraction.text.CountVectorizer