日本語で「自然言語処理」するための必要最低限の知識を自分なりに整理することにします。
はじめに
自然言語処理の必要最低限の知識を自分なりに整理してみることにします。
長くなるので、3回位にわけて記事には書きます。
ちなみに各回のテーマは。
(1)単語分割(形態素解析)と前処理 <今回>
(2)単語の出現頻度に着目した文章データの数値化<8/9予定>
(3)単語のベクトル化
です。
以下のような件に対応することを想定しています。
- 含まれる単語等に着目して、テキストデータの特徴を数値化する。
- 特徴を数値化したデータで学習し、分類・予測(文生成含む)する。
含まれる単語等に着目して、テキストデータの特徴を数値化する
自然言語処理では、文章を単語単位に分割する必要があります。
この「単語に分割する」が、英語と違い、日本語ではとても困難です。
英語は単語ごとに空白で区切られていますが、日本語の文章には単語毎の区切りがありませんから。
例えば。
I grew up in the countryside.
みたいな英文を「I」「grew」「up」「in」「the」「countryside」に分けるなら、半角空白でSplitすれば良いだけなので、超簡単です。
でも、同じ意味の
私は田舎で育ちました。
を単語に分割するのは難しい。
英語みたいに明確な区切り文字(英語だと半角空白)がありません。
そのうえ、同じ文字でも前後の単語との関係で品詞が変わり、区切られる位置も変わる日本語の特性もあります。
でも、有難いことに先人の研究で、自分らでも使えるツールが存在します。
ここは有難く、研究の成果を使わせていただくだけにしようと思います。
文章を単語に分割して、品詞を割り当ててくれるツールの主なものです。
MeCab
多分、一番有名で利用されているものです。
なんと、EXCELのVBAで使えるものを公開してくれている方とかも居ます。
Janome
pythonで使うなら一番手軽に使えます。
辞書はMeCabと同じものが標準で同梱されてます。
速度が遅いとか言われてますが、正直、自分程度の使い方なら、MeCabと比べて遅いなと感じることはほとんどないです。
以降の説明では、Janomeを使って行っていきます。
形態素解析した結果ってこんな感じ
janomeを使って、前に例で出した「私は田舎で育ちました」をやってみます。
# -*- coding: utf-8 -*- from janome.tokenizer import Tokenizer t = Tokenizer() malist = t.tokenize("私は田舎で育ちました。"); for n in malist: print(n.surface + ":" + n.part_of_speech);
結果はこんな感じです。
私:名詞,代名詞,一般,*
は:助詞,係助詞,*,*
田舎:名詞,一般,*,*
で:助詞,格助詞,一般,*
育ち:動詞,自立,*,*
まし:助動詞,*,*,*
た:助動詞,*,*,*
。:記号,句点,*,*
単語に分かれて、品詞情報が付与されているという意味合いが、よくわかります。
英語ベースの説明だと、単語に分割したら、いきなり後続の処理(ベクトル化とか)が始まりますが、日本語だと、そういうわけにはいきません。
2つの理由があります。
- 文章を特徴づけるのに不要な文字(単語)が含まれていること。
- 同じ意味なのに異なる文字で表現されている言葉がたくさんあること。
文章を特徴づけるのに不要な文字(単語)が含まれていること
例えば、前の「私は田舎で育ちました。」なら、この文章の特徴を強く表現するのは「名詞・動詞」である「私・田舎・育ち」の3語です。
助詞の「は」「で」とか、記号の「。」とかは文章の特徴にあまり関係しません。
なので、単純に文章の特徴をとらえて何かできればよいのなら、「名詞」「動詞」だけを残して、他の品詞を取り除いてから、後続処理に渡す方が効率が良いです。
ですが、例えばネガポジ判定とかをしたいとなると、話が変わってきます。
同じ名詞・動詞が登場しても、ネガの場合もあれば、ポジの場合もありえますので。
例えば、前の例を否定文にしてみます。
私は田舎以外で育ちました。
この例なら、否定の「以外」が名詞なので、なんとかいけますけど。
私:名詞,代名詞,一般,*
は:助詞,係助詞,*,*
田舎:名詞,一般,*,*
以外:名詞,非自立,副詞可能,*
で:助詞,格助詞,一般,*
育ち:動詞,自立,*,*
まし:助動詞,*,*,*
た:助動詞,*,*,*
。:記号,句点,*,*
私は田舎で育ちませんでした。
これだと、名詞と動詞だけ抜き出したら同じ文章になってしまいますので、否定にあたる助動詞の部分も判断に必要になったりします。
私:名詞,代名詞,一般,*
は:助詞,係助詞,*,*
田舎:名詞,一般,*,*
で:助詞,格助詞,一般,*
育ち:動詞,自立,*,*
ませ:助動詞,*,*,*
ん:助動詞,*,*,*
でし:助動詞,*,*,*
た:助動詞,*,*,*
。:記号,句点,*,*
このように、何をしたいか・・によって、どの品詞を残すかの検討が必要です。
目指す品詞だけを処理する前処理の簡単に例を書くとすると。
# -*- coding: utf-8 -*- from janome.tokenizer import Tokenizer import re #正規表現 pat = r'名詞|動詞|助動詞' regex = re.compile(pat) t = Tokenizer() malist = t.tokenize("私は田舎で育っていません。") for n in malist: if(regex.match(n.part_of_speech)): print(n.surface + ":" + n.part_of_speech)
こんな感じです。
上記を実行すると、指定の品詞のみが抽出されます。
私:名詞,代名詞,一般,*
田舎:名詞,一般,*,*
育っ:動詞,自立,*,*
い:動詞,非自立,*,*
ませ:助動詞,*,*,*
ん:助動詞,*,*,*
基本、この応用でいけるはずです。
同じ意味なのに異なる文字で表現されている言葉がたくさんあること
例えば、「私は田舎で育ちました。」の「私」ひとつとっても、「わたし」「ワタシ」「俺」「僕」「おいら」「自分」など・・バリエーションは沢山あります。
意味合い的には「私は田舎で育ちました」と「わたしはいなかでそだちました」と「僕はイナカで育ちました」は、まったく同じなんですが、文字コードで学習・判定させてしまうと、まったく違うものとして扱われてしまいます。
自分のやりたいことに、この違いがどう影響するか?
これをよく考えないといけません。
「私」「わたし」「ワタシ」「僕」「俺」「自分」を、同一のものとして扱わないと具合が悪い場合なら、前処理で置き換えて、例えば「私」に無理やり統一してしまうなどが必要になります。
例えば、こんな感じですかね。
# -*- coding: utf-8 -*- from janome.tokenizer import Tokenizer d = {"僕":"私","俺":"私","自分":"私"} t = Tokenizer() malist = t.tokenize("俺は田舎で育ちました。") for n in malist: if(n.surface in d): print(d[n.surface] + ":" + n.part_of_speech) else: print(n.surface + ":" + n.part_of_speech)
実行すると、俺を私に置き換えて出力されます。
私:名詞,代名詞,一般,*
は:助詞,係助詞,*,*
田舎:名詞,一般,*,*
以外:名詞,非自立,副詞可能,*
で:助詞,格助詞,一般,*
育ち:動詞,自立,*,*
まし:助動詞,*,*,*
た:助動詞,*,*,*
。:記号,句点,*,*
実際には、辞書ファイルを「俺、私」みたいなCSVファイルででも用意しておいて、それを読み込んでディクショナリを初期化する感じにはなるでしょうが、簡単・シンプルにやれます。
逆に、やりたいことが「各人の持つ文章の癖を識別して、文章から個人特定したい」みたいな話なら、一人称の書き方だけでなく、句読点の打ち方とか、文章のすべてが特徴(癖)になるので、置き換えなどする必要はないと判断できます。
こんな感じで、ここでも「何をやりたいのか」ありきで、対処方法は180度違い、学習精度にも大きな影響がでることになります。
2018/08/06追記です
とても重要なことを忘れていました。
処理対象にするテキストデータの文字コードを揃えておくことです。
ランダムにテキストファイルを集めると、あるファイルはSJIS、あるファイルはUTF8みたいなことになって、変なところでつまづきます。
特に理由がなければ、UTF8にそろえておくと一番面倒が少ないです。
経験上。
当たり前のことではありますけど、追記しておきます。
長くなるので続きます
形態素解析と前処理ができたら、次は数値化の処理です。
こっからも結構長くなるので、「簡単に考えてみる(2)」に続きます。