neologdを利用するにあたってのテキストの前処理について

はじめに

最近、sklearnを使ってtf-idfをを計算するというのをしているのですが、この計算の前に日本語のテキストの分かち書きをしないといけません。 しかし、この分かち書きがなかなか上手くいきません。 例えば、「カラーブラック」というテキストがあったときに、「カラー、ブラック」に分かち書きしてほしいのですが、現在分かち書きで利用しているMeCabのデフォルトの辞書では上手くいきません。mecab-ipadic-neologdも試しましたが、この辞書は新語が登録されているだけなので、例のように分かち書きはできませんでした。 また、Web上のテキストには、URLやメールアドレス含まれます。日本語の場合には、半角全角の英数字やカタカナ、ひらがなや漢字など様々なパターンが入り混じっているため、これらを前処理の段階で適切に処理しておかなければ、辞書の単語とマッチしないため分かち書きが上手くいきません。 このことから、単語を辞書に登録することも重要ですが、形態素解析を行う前処理が重要ということが分かりました。 では、どのような前処理を行っておけばよいのだろうか?と考えて調べたところ、mecab-ipadic-neologdを適用する場合のテキストの正規化のやり方が書かれているページを発見しました。 この前処理は、neologdの辞書生成時に行っているものと同じなので、辞書とマッチしやすくなります。 今回はneologdを利用するときに推奨されている前処理についてコードの解説をまとめておこうと思いました。

前処理のコード

Regexp.ja · neologd/mecab-ipadic-neologd Wiki · GitHubより抜粋。

# encoding: utf8
from __future__ import unicode_literals
import re
import unicodedata

def unicode_normalize(cls, s):
    pt = re.compile('([{}]+)'.format(cls))

    def norm(c):
        return unicodedata.normalize('NFKC', c) if pt.match(c) else c

    s = ''.join(norm(x) for x in re.split(pt, s))
    s = re.sub('-', '-', s)
    return s

def remove_extra_spaces(s):
    s = re.sub('[  ]+', ' ', s)
    blocks = ''.join(('\u4E00-\u9FFF',  # CJK UNIFIED IDEOGRAPHS
                      '\u3040-\u309F',  # HIRAGANA
                      '\u30A0-\u30FF',  # KATAKANA
                      '\u3000-\u303F',  # CJK SYMBOLS AND PUNCTUATION
                      '\uFF00-\uFFEF'   # HALFWIDTH AND FULLWIDTH FORMS
                      ))
    basic_latin = '\u0000-\u007F'

    def remove_space_between(cls1, cls2, s):
        p = re.compile('([{}]) ([{}])'.format(cls1, cls2))
        while p.search(s):
            s = p.sub(r'\1\2', s)
        return s

    s = remove_space_between(blocks, blocks, s)
    s = remove_space_between(blocks, basic_latin, s)
    s = remove_space_between(basic_latin, blocks, s)
    return s

def normalize_neologd(s):
    s = s.strip()
    s = unicode_normalize('0-9A-Za-z。-゚', s)

    def maketrans(f, t):
        return {ord(x): ord(y) for x, y in zip(f, t)}

    s = re.sub('[˗֊‐‑‒–⁃⁻₋−]+', '-', s)  # normalize hyphens
    s = re.sub('[﹣-ー—―─━ー]+', 'ー', s)  # normalize choonpus
    s = re.sub('[~∼∾〜〰~]', '', s)  # remove tildes
    s = s.translate(
        maketrans('!"#$%&\'()*+,-./:;<=>?@[¥]^_`{|}~。、・「」',
              '!”#$%&’()*+,-./:;<=>?@[¥]^_`{|}〜。、・「」'))

    s = remove_extra_spaces(s)
    s = unicode_normalize('!”#$%&’()*+,-./:;<>?@[¥]^_`{|}〜', s)  # keep =,・,「,」
    s = re.sub('[’]', '\'', s)
    s = re.sub('[”]', '"', s)
    return 

前処理の解説

上記した前処理のコードについて解説していきます。

unicode正規化

def unicode_normalize(cls, s):
    pt = re.compile('([{}]+)'.format(cls))

    def norm(c):
        return unicodedata.normalize('NFKC', c) if pt.match(c) else c

    s = ''.join(norm(x) for x in re.split(pt, s))
    s = re.sub('-', '-', s)
    return s

# main
s = unicode_normalize('0-9A-Za-z。-゚', s)

unicodeでは、仮名の濁音や半濁音などを表すために、合成済み文字、もしくは結合文字列(基底文字の後に1以上の結合文字を続けた文字列)を利用する。例えば、合成済み文字だと「だ」と表し、結合文字列だと「た+゛」と表すことがある。 また、他の符号化された標準的な文字と実際的には同等な文字を符号化していたりと、様々な表現が混在している。 このような様々な表現が混在した文字列をある基準で統一するのがunicode正規化です。 正規化の方法は4種類定義されていますが、詳しくはUnicode正規化とはを参照してください。

上記のコードでは、正規表現0-9A-Za-z。-゚で全角英数字や記号を見つけて、unicodedata.normalize('NFKC', c)でNFKCという正規化形式で正規化することで、半角英数字などに変換を行っています。

そして、最後に全角のハイフン(ー)を半角のハイフン(-)に変換しています。

余分なスペースの削除

def remove_extra_spaces(s):
    s = re.sub('[  ]+', ' ', s)
    blocks = ''.join(('\u4E00-\u9FFF',  # CJK UNIFIED IDEOGRAPHS
                      '\u3040-\u309F',  # HIRAGANA
                      '\u30A0-\u30FF',  # KATAKANA
                      '\u3000-\u303F',  # CJK SYMBOLS AND PUNCTUATION
                      '\uFF00-\uFFEF'   # HALFWIDTH AND FULLWIDTH FORMS
                      ))
    basic_latin = '\u0000-\u007F'

    def remove_space_between(cls1, cls2, s):
        p = re.compile('([{}]) ([{}])'.format(cls1, cls2))
        while p.search(s):
            s = p.sub(r'\1\2', s)
        return s

    s = remove_space_between(blocks, blocks, s)
    s = remove_space_between(blocks, basic_latin, s)
    s = remove_space_between(basic_latin, blocks, s)
    return s

まず、全角・半角スペースが複数回連続する部分を正規表現で単一の半角スペースに置き換えています。

次にblocksとbasic_latinという変数に以下の正規表現が宣言されています。

正規表現 意味
\u4E00-\u9FFF すべての漢字
\u3040-\u309F 全てのひらがな
\u30A0-\u30FF 全てのカタカナ
\u3000-\u303F 記号
\uFF00-\uFFEF 半角カタカナ全角英数字など
\u0000-\u007F 基本文字(制御文字、半角英数字・記号など)

それぞれの詳細はUnicodeを参照してください。

最後に、remove_space_between()関数にblocksとbasic_latinを組み合わせて渡すことで、文字と文字の間にスペースが入っていた場合に、スペースを削除しています。

neologdの正規化

def normalize_neologd(s):
    s = s.strip()
    s = unicode_normalize('0-9A-Za-z。-゚', s)

    def maketrans(f, t):
        return {ord(x): ord(y) for x, y in zip(f, t)}

    s = re.sub('[˗֊‐‑‒–⁃⁻₋−]+', '-', s)  # normalize hyphens
    s = re.sub('[﹣-ー—―─━ー]+', 'ー', s)  # normalize choonpus
    s = re.sub('[~∼∾〜〰~]', '', s)  # remove tildes
    s = s.translate(
        maketrans('!"#$%&\'()*+,-./:;<=>?@[¥]^_`{|}~。、・「」',
              '!”#$%&’()*+,-./:;<=>?@[¥]^_`{|}〜。、・「」'))

    s = remove_extra_spaces(s)
    s = unicode_normalize('!”#$%&’()*+,-./:;<>?@[¥]^_`{|}〜', s)  # keep =,・,「,」
    s = re.sub('[’]', '\'', s)
    s = re.sub('[”]', '"', s)
    return s

この部分はneologdで行っている正規化処理を表しています。 まず、strip()で文字列の前後の空白を取り除き、unicode_normalize()でunicode正規化を行っています。 次に、re.sub()を使って、ハイフンやチルダなどの記号を全角の記号に置き換えています。 次に、余分なスペースを削除し、全角記号群をunicode正規化をしています。 最後に、全角のクォート、ダブルクォートを半角英数字に置き換えています。

Web上のテキスト特有の処理

Web上のテキストは、URLやメールアドレス、罫線などを含んでいる場合があるため、これらを削除する前処理が必要です。 以下のような正規表現でマッチングすることができます。

url_re = re.compile("https?://[\w/:%#\$&\?\(\)~\.=\+\-@]+")
mail_re = re.compile("[\w\-\.]+@[a-zA-Z0-9\-\.]+\.[a-zA-Z]+")
rule_line_re = re.compile("\-{2,}|ー{2,}|-{2,}"

参考