
こんにちは、AI研究所見習い研究員のショウです。
今回は、自然言語処理に重要なデータの前処理について解説していきます。
形態素解析
まず、自然言語処理において大事な形態素解析から説明していきます。
形態素解析とは
一つ文、例えば「すもももももももものうち」
といった言葉があります。
これを形態素解析すると
「すもも」「も」「もも」「も」「もも」「の」「うち」
となり、
このように「意味が分かるような、最小単位に分ける」
ことを形態素解析、分かち書きにすると言います。
何故このような形にするかというと、
英語は単語ごとに区切られているので
単語同士の関係性がわかりやすいですが、
日本語の場合だと、どこでどの単語が区切られているかが
ぱっと見ではわかりません。
自然言語処理などでは、
単語をベクトル表現することが多く、
そのベクトル表現のために、どうしても単語ごとの区切りが必要になってきます。
単語のベクトル表現
それではどのようにして、単語をベクトル表現していくのか説明します。
例えば上の「すもも」「も」「もも」「も」「もも」「の」「うち」は
全部で7つの区切りです。
そこで最初に7つ全てに単語IDをつけます。
「すもも」ー>ID1
「も」ー>ID2
「もも」ー>ID3
「も」ー>ID2
「もも」ー>ID3
「の」ー>ID4
「うち」ー>ID5
重複している言葉、
つまり、「も」や「もも」は同じIDになります。
One Hot Vector
続いて、このIDがついた単語から
One Hot Vectorといった、単語をベクトルで表現したものを作っていきます。
例えば上の単語は全部で5つのIDで構成された
辞書と見ることができます。
そこで、単語IDの数だけゼロで埋めた
ベクトルを用意します。
「0、0、0、0、0」
続いて、単語ID1の「すもも」は
「1、0、0、0、0」
という「固有」のベクトルにします。
そうやって、各IDごとにベクトルを作っていくと、
単語ID | 単語 | One Hot Vector |
---|---|---|
ID1 | すもも | [1,0,0,0,0] |
ID2 | も | [0,1,0,0,0] |
ID3 | もも | [0,0,1,0,0] |
ID4 | の | [0,0,0,1,0] |
ID5 | うち | [0,0,0,0,1] |
表のように、各単語が固有のベクトルで表現できるようになります。
Pythonで試す
上の説明の流れをPythonでやってみたいと思います。
尚、今回形態素解析に使用するライブラリは「janome」を使います。
理由は「pip」で簡単にインストールできて、お手軽に試せるからです。
ローカル環境になければインストールします。
pip install janome
続いて、分かち書きを行います。
# janomeをインポート from janome.tokenizer import Tokenizer # 形態素解析をするインスタンスを作成。 t = Tokenizer() # 例文は「すもももももももものうち」。 s = 'すもももももももものうち' # 解析します。 tokens = t.tokenize(s) # トークンオブジェクトのリストから一つずつ解析結果をプリントします。 for token in t.tokenize(s): print(token) """ すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ も 助詞,係助詞,*,*,*,*,も,モ,モ もも 名詞,一般,*,*,*,*,もも,モモ,モモ も 助詞,係助詞,*,*,*,*,も,モ,モ もも 名詞,一般,*,*,*,*,もも,モモ,モモ の 助詞,連体化,*,*,*,*,の,ノ,ノ うち 名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ """
各トークンオブジェクトには、形態素や品詞、読みといった情報が入っています。
品詞ごとに分けたい場合はこの品詞情報を使います。
とりあえず、オブジェクトのsurface属性から、分かち書きの結果だけを取り出します。
import numpy as np # 分かち書きの結果の単語をリストにします。 word_list=[token.surface for token in tokens] # IDから単語へ、逆に単語からIDへアクセスできるよう2つの辞書を作ります。 i2w=dict((i,w) for i,w in enumerate(sorted(list(set(word_list))))) w2i=dict((w,i) for i,w in enumerate(sorted(list(set(word_list))))) # 確認してみましょう。 print(i2w) """ {0: 'うち', 1: 'すもも', 2: 'の', 3: 'も', 4: 'もも'} """ print(w2i) """ {'うち': 0, 'すもも': 1, 'の': 2, 'も': 3, 'もも': 4} """ # 語彙数を辞書の長さから取得します。 vocab_size=len(w2i) # 単語リストをNumpy配列にします。 N=np.array(list(w2i.values())) # 単語数 x 語彙数 のゼロ埋め行列を作ります。 one_hot = np.zeros((N.shape[0], vocab_size), dtype=np.int32) print(one_hot) """ [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]] """ # 行には単語が割り振られてる考え、 # 列に一意の数字を割り振ると考えます。 for i, word_id in enumerate(list(w2i.values())): one_hot[i, word_id] = 1 print(one_hot) """ [[1 0 0 0 0] [0 1 0 0 0] [0 0 1 0 0] [0 0 0 1 0] [0 0 0 0 1]] """
列には一つずつズレて1がつけられています。
各行は同じ値をもたない固有のベクトル表現とみることができます。
このように、単語の関係性を数値化することによって、
ニューラルネットワークなどで学習ができるようなります。
細かな前処理
本来ならこの章の細かな前処理をおこなってから、
単語や文のベクトル表現をしますが、
学習の種類、目的によって使わないことも含まれているので
後回しにしました。
正規化
日本語には「2」と「2」、「ネコ」と「ネコ」など
半角文字と全角文字があります。
これらが混ざっていると、同じ意味なのに違う単語IDに振り分けられてしまうことが起こります。
それを防ぐために
単語辞書を作るのと同時に、または分かち書きする前に
正規化してしまいましょう。
上のPyPiリンクのライブラリは
半角や全角文字を統一してくれる便利な機能をもっています。
これを使って例を示します。
# まずインストール pip install neologdn
# neologdnをインポート import neologdn # 半角のカナを全角に直します。 neologdn.normalize("ネコカワイイ") # => 'ネコカワイイ' # 全角の記号を半角に直します。 neologdn.normalize("・()「」!?@#") # => '・()「」!?@#' # 長音短縮 # 様々な長さのウェーイを統一します。 weei=["ウェーーーーイ","ウェーーイ","ウェーーーーーーーーイ"] for w in weei: print(neologdn.normalize(w)) # => ウェーイ # ウェーイ # ウェーイ # チルダを削除できます。 neologdn.normalize("ウェ~∼∾〜〰~イ") # => 'ウェイ' # ハイフンの統一 neologdn.normalize("˗֊‐‑‒–⁃⁻₋−") # => '-' # 全角英数字を半角に直し、スペースを詰めます。 neologdn.normalize(" Python 必 読 書 ") # => 'Python必読書' # 同じ文字の繰り返しを何回にするかを指定できます。 neologdn.normalize("勇者あああああああよ", repeat=4) # => '勇者ああああよ' # 色々組み合わせることができます。 # '勇者「ああああ」よ。にしたい。' text='勇者「ぁぁぁあああ~~~ぁああぁああ」よ。' text_normalized=neologdn.normalize(text) print(text_normalized) # => 勇者「ぁぁぁあああぁああぁああ」よ。 text_normalized=text_normalized.replace('ぁ','') print(text_normalized) # => 勇者「あああああああ」よ。 text_normalized=neologdn.normalize(text_normalized,repeat=4) print(text_normalized) # => 勇者「ああああ」よ。 # 全角「」が変換されないので、 # Replaceメソッドを使う text_normalized=text_normalized.replace('「','「').replace('」','」') print(text_normalized) # => 勇者「ああああ」よ。 # ReplaceメソッドやReライブラリ等で補っていけば良いと思います。
絵文字
絵文字も除去する必要がある場合があります。
上のPyPiリンクのライブラリは
絵文字を取り扱えるようにしてくれる便利な機能をもっています。
これを使って例を示します。
# まずインストール pip install emoji --upgrade
chars=['😀','😃','😄'] codes=[emoji.demojize(c) for c in chars] print(codes) # => [':grinning_face:',':grinning_face_with_big_eyes:',':grinning_face_with_smiling_eyes:'] # 絵文字の除去 with_emoji=['グリーティングフェイス😀ビッグアイズ😃スマイリングフェイス😄'] remove_emoji = ''.join(list(filter(lambda x: x not in emoji.UNICODE_EMOJI, test))) print(remove_emoji) # => 'グリーティングフェイスビッグアイズスマイリングフェイス'
Stop Word
Stop Wordとは、全文検索などで一般的すぎて
検索の邪魔になる単語をいいます。
英語なら「The」や「a」など
日本語なら「て」「に」「を」「は」などです。
これらを取り除くことで、
計算量の節約、学習精度を上げることができます。
ただ、個人の意見ですが、
あくまで全文検索、つまり検索エンジンのアルゴリズムから
きているので、100%自然言語処理の学習において、正しいとは言い切れません。
なぜなら、「私は今日朝10時に起きた。」
こんな文があり、
「私に今日を朝10時に起きた。」や
「私を今日は朝10時に起きた。」になると
不自然な文になります。
色々考え方ややり方があると思いますが、
とりあえずStop Wordの除去をPythonで
実装してみましょう。文章の題材はここまでの説明文にします。
import urllib from janome.tokenizer import Tokenizer # ストップワードをダウンロード url = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt' urllib.request.urlretrieve(url, 'stop_word.txt') with open('stop_word.txt', 'r', encoding='utf-8') as file: stop_word_list = [word.replace('\n', '') for word in file.readlines() if word.replace('\n', '')!=''] print(stop_word_list) # => ['あそこ', 'あたり',.........,'同じ', '感じ'] # 上の説明文をそのまま文字列として格納する。 text_data='Stop Wordとは、.......省略....... Pythonで実装してみましょう。' # 分かち書きを行う。 # 形態素解析をするインスタンスを作成。 t = Tokenizer() # 解析します。 tokens = t.tokenize(text_data) # 分かち書きの結果の単語をリストにします。 word_list=[token.surface for token in tokens] split_stop_word=[] for w in word_list: if w not in stop_word_list and w != ' ': split_stop_word.append(w) print(split_stop_word) # => ['Stop', 'Word', '、', '全文', '検索',.........,'Python', '実装', 'み', 'ましょ', '。']
品詞ごとに分類
続いて品詞、例えば「名詞」「形容詞」「動詞」に絞って分類してみます。
感情分析や、レコメンドシステムを作る際に便利です。
# pandasをインポートします。 import pandas as pd # 先程と同様、janomeによる形態素解析を行う。 tokens = t.tokenize(text_data) # 名詞、形容詞、動詞に分けてデータフレームに直す関数を作る。 def split_part_of_speech(tokens): word_list=[] for token in tokens: pos=token.part_of_speech.split(',') if '名詞' == pos[0] or '形容詞' == pos[0] or '動詞' == pos[0]: word_list.append([token.surface,pos[0]]) return pd.DataFrame(word_list,columns=['単語','品詞']) # 使ってみる。 df=split_part_of_speech(tokens) print(df) """ 単語 品詞 0 Stop 名詞 1 Word 名詞 ............... 75 除去 名詞 76 Python 名詞 77 実装 名詞 78 し 動詞 79 み 動詞 """ # さらにStop Wordも併用してみます。 remove_stop_word_df=df[df['単語'].apply(lambda x: x not in stop_word_list)==True] print(df.shape) # => (80, 2) # 12単語減りました。 print(remove_stop_word_df.shape) # => (68, 2) # indexが飛び飛びなので、修正します。 remove_stop_word_df=remove_stop_word_df.reset_index(drop=True) print(remove_stop_word_df) """ 単語 品詞 0 Stop 名詞 1 Word 名詞 2 全文 名詞 ................... 64 Python 名詞 65 実装 名詞 66 し 動詞 67 み 動詞 """
おわりに
今回は以上ですが、
その他にもTF-IDFによる、単語頻出度による振り分けなど、
様々な前処理の方法があります。
文章や言語、目的によって前処理の方法も違ってきます。
様々な文章にチャレンジして、適切な前処理を身につけてください。
もっと深く理解されたい方は、ビジネス向けAI完全攻略セミナーを受講してみてください。
最後までお読み頂きありがとうございました。