
前処理、前処理、そして、前処理 (自然言語処理:日本語編)
こんにちは!エンジニアのnaruです。
ブラックフライデーも終わり皆さま何か良いものは買えましたでしょうか?
私はというと、9月10月とAppleの新製品とモニターに散財して懐が早くも氷点下になって消費欲が完全に満たされたのもあり、ブラックフライデーは何も響かなかったです笑
さて、そんな最近に至るまで業務でたまに日本語テキストの前処理を行うことがあるのですが、その度に「あれ、これどうやるんだっけ・・・」となっていたので整理してみます。
「山田く〜ん、このデータ良い感じに前処理しといて〜」といきなり言われてしまった方になにか参考になれば幸いです。
前処理とは
読んで字のごとく前処理とは、後続の処理をやりやすく、そして精度良くすることを目的とした必要不可欠かつ重要な処理です。
具体的な処理内容としては多岐にわたり、処理対象となる文章そのものの"クセ"や後続の処理への理解が求められるものの、時間がかかる割に派手さがない作業なので自分はまだ好きになりきれずにいます。(こんな記事書いといて〜というのは一旦忘れてください笑)
前提
検証環境
Intel Macbook Pro (macOS BigSur 11.6)
Python 3.7.4
前処理の全体の流れ
前処理は以下のステップで進めていきます。
1. 不要な文字列削除
2. ルールに沿った文字列の変換
3. 単語への分割
4. 必要な単語の抽出
1. 不要な文字列削除
文章から解析に不要な文字列をどんどん除去していきます。
このとき、"何か不要か?"はプロジェクトにより変わると思うので、適宜除去対象を取捨選択します。
基本正規表現で文字列ヒットさせて空文字列かスペースに置換させる方針が一般的かと思いますが、便利なライブラリやメソッドがあった場合はそちらを使っています。(正規表現を用いる場合は、こちらの正規表現確認ツールが便利でした)
改行コードの削除
改行コードには、CR(\r)、LF(\n)、CR+LF(\r\n)が種類としてはあるため、下記のように "\r" と "\n" のどちらも削除する方針を取ります。
text = 'ヤッホー!\n元気かな?\nおらは元気だぞ\n'
text = text.replace('\n','').replace('\r','')
# text => 'ヤッホー!元気かな?おらは元気だぞ'
なお、上記の他には splitlinesメソッドで分割する方法などがあります。
URL削除
こちらは正規表現で削除します。reライブラリを使います。
import re
text = re.sub(r'http?://[\w/:%#\$&\?\(\)~\.=\+\-]+', '', text)
text = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-]+', '', text)
絵文字の削除
demojiライブラリを使って削除します。
import demoji
text = demoji.replace(string=text, repl='')
半角記号削除
こちらは羅列して削除します。半角ハイフン "-" については"Wi-Fi"など単語としてハイフン込みで成り立っているものがあるため、あえて入れていません。
text = re.sub(r''[!"#$%&\'\\\\()*+,-./:;<=>?@[\\]^_`{|}~「」〔〕“”〈〉『』【】&*・()$#@。、?!`+¥%]', '', text)
全角記号削除
これも便利なライブラリなどが見つからず、こちらなどを参考にさせていただいて削除。(文字コードまわり複雑・・・)
text = re.sub("[\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\u3000-\u303F]", '', text)
2. ルールに沿った文字列の変換
全角英数字を半角に変換・1回以上連続する長音記号を1回に変換(その他正規化処理)
3. の単語への分割時に使う辞書ファイル mecab-ipadic-NEologd で推奨される正規化処理を neologdn ライブラリを利用して行います。
インストールはpipコマンドで一発で入ります。
pip install neologdn
neologdn の normalize メソッドは全角英数字を半角に変換したり、「キターーーーー」といった連続した長音記号を削除して「キター」にする、などの変換をしてくれます。
import neologdn
text = neologdn.normalize(text)
桁区切り数字を 0 に変換
12,345,678 のようなコンマ付きの数字を全て 0 に変換します。
text = re.sub(r'\b\d{1,3}(,\d{3})*\b', '0', text)
数値を全て 0 に変換
数値が重要な意味を持つような後続処理でなければ、全て 0 にならしてしまいます。
text = re.sub(r'\d+', '0', text)
大文字を小文字に統一
大文字・小文字どちらに寄せるかは悩みどころですが、表記揺れしないようにどちらかに統一します。(ここでは小文字に寄せます)
text = text.lower()
3. 単語への分割
MeCab + mecab-ipadic-NEologd の導入
英語のようにスペースで単語が区切られていない日本語は、まず単語単位に文章を分割する必要があります。そこで行うのが形態素解析と呼ばれる「文章を意味ある最小単位まで分解して品詞・意味を割り出す処理」を行います。
形態素解析には一番メジャーだと思われるMeCabを使います。MeCabは形態素解析"エンジン"であり、辞書などはデフォルトのものを利用することもできますし、外部の他の辞書を利用することもできます。
MeCab導入はpipコマンドで一発です。
pip install mecab-python3
ここでは、 辞書としてmecab-ipadic-NEologdを利用することとします。
mecab-ipadic-NEologd は形態素解析エンジン MeCab と共に使う単語分かち書き辞書で、週2回以上更新更新され、新語・固有表現に強く、語彙数が多く、しかもオープンソース・ソフトウェアである という特徴があります。
参考:https://engineering.linecorp.com/ja/blog/mecab-ipadic-neologd-new-words-and-expressions/
MeCab とともに mecab-ipadic-NEologd の辞書を使うことによる効果として、「リアル二刀流」など新しい単語などが「リアル/二刀流」などと分割されずにきちんとひとつの単語として認識されるようになったりします。(詳細は上記参考サイトを参照ください)
mecab-ipadic-NEologd 導入についてはこちらの公式の日本語READMEを参考にして進めます。
形態素解析する
以下のようにします。
import MeCab
# 解析対象の文字列を用意
text = "リアル二刀流とはなんぞや"
# 辞書を指定してタガー生成
tagger = MeCab.Tagger("-Ochasen -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd")
# 形態素解析結果を出力
for line in tagger.parse(text).splitlines():
print(line)
結果は以下の通り出力されます。
リアル二刀流 リアルニトウリュウ リアル二刀流 名詞-固有名詞-一般
と ト と 助詞-格助詞-引用
は ハ は 助詞-係助詞
なんぞ ナンゾ なんぞ 助詞-副助詞
や ヤ や 助動詞 特殊・ヤ 基本形
EOS
タガー生成時に指定している -Ochasen とはMeCabの解析結果の出力モードの一つで、指定しない場合はデフォルトの出力になりますが、4. の必要な単語の抽出でChaSen品詞体系と呼ばれる品詞体系で絞り込みを行う予定なのでChaSen互換形式の-Ochasenを指定します。
4. 必要な単語の抽出
品詞による抽出
文章を単語単位で分割したところで、多くの場合、全ての単語が必要なわけではありません。
そこで、品詞により必要な単語かどうかの選別を行います。
3. ではChaSen品詞体系で品詞を出力したため、品詞体系の表を見ながら、必要そうな品詞の種類をリスト化します。
ChaSen品詞体系の表はこちらを参照させていただきました。
https://www.unixuser.org/~euske/doc/postag/#chasen
この中で自分が意味ありそうとリスト化してみたものが以下になります。
target_parts_of_speech = [
"名詞-サ変接続",
"名詞-形容動詞語幹",
"名詞-一般",
"名詞-固有名詞-一般",
"名詞-固有名詞-組織",
"形容詞-自立"
]
ストップワードの除去
上記で品詞によって抽出した単語の中にもどうしても不要な単語が混じってしまいます。
そこでストップワードと呼ばれる、"一般的すぎる"単語をリストとして持っておき(これをストップリストと呼ぶ)、それらを除外する処理をここでは実施します。(その他には出現頻度などで除外する方式などがある)
一般的な用語のストップリストは例えばSlothLibプロジェクトから取得することができます。(日本語ストップリスト)
こちらのストップリストだけでなく、自分が処理したいテキストの特性として他にも除外したい単語が出てきた場合は、自分でストップリストを作成することも必要だと思っています。
実際に抽出してみる
品詞による抽出とストップワードの除去を行う実装は例えば以下のようになります。
# 処理対象の文章
text = "リアル二刀流とはなにかを知るため我々はアマゾンの奥地へと向かった"
# SlothLibプロジェクトから取得したストップリストの読み込み
with open('./japanese_stop_words.txt') as f:
stop_words = f.read().splitlines()
result_word_list = []
for line in tagger.parse(text).splitlines():
if line is None or line == '' or line == 'EOS' or len(line.split()) < 4:
continue
for target_part_of_speech in target_parts_of_speech:
if target_part_of_speech == line.split()[3]:
word = line.split()[2]
if not word in stop_words:
result_word_list.append(word)
print(result_word_list)
# ['リアル二刀流', 'アマゾン', '奥地']
何かそれっぽい結果が出ましたー!やったね!
最後に
本記事で記載してある前処理の他にも色々な処理や方法があり、前処理だけでも書籍が出ているくらい意外と奥深い世界なので、これから前処理道をもっと極めていければと思ってます!
そして、最後に弊社スペースマーケットではエンジニアの新メンバーを募集しておりますので、もしご興味があるようでしたら是非お話しだけでも!