【ソースコード付き】日本語テキストマイニングを行うために必要な前処理
06
- 8月
2017
Posted By : boomin
【ソースコード付き】日本語テキストマイニングを行うために必要な前処理

 

Advertisements

はじめに

日本語のテキストマイニングを粉うには、いろいろと前処理が必要となります。
日本語じゃなくても必要なのですが、特に日本語の場合、文章内で単語が分かれていないために、
単語ごとに分解する「分かち書き」が必要となります。

 

 
これは、全世界リアルタイム麺活監視システムで表示される、監視上法の1つです。
世界の麺活erたちがtwitterでつぶやいた単語を分かち書きして、単語に分解したあと、
よく使われる単語をより大きく表示させる、タグクラウド と呼ばれる可視化手法の一つです。
こうすることで、よく使用される単語や、関連する単語を浮かび上がらせることができます。

しかし、そもそも分かち書きするためには、辞書をもとに品詞分解しないといけないため、
それをそれなりの精度と速度で実行するためには、ライブラリに頼らざるを得ません。

そこで今回、ライブラリの一つであるMecabを使って、うまいこと前処理するための
なんちゃってライブラリを作りました。

 

What is this?

日本語解析ライブラリMeCabを使う際の、前処理を行うためのpython用ライブラリです。

前処理を行い、対象とする文章がどのようなものなのかを把握するために、
統計的手法を用いて、その内容を把握するのです。

例えば以下のものは、形態素解析した後、よく使用される単語ランキングを可視化したものです。
こうした可視化や文章の定量的理解も、前処理を行わなければできません。

 

 
こちらも、全世界リアルタイム麺活監視システムで監視されている情報の1つです。
twitterの本文を品詞ごとに形態素解析を行い、そのうえで、使用頻度の高い単語数を集計して、
頻度順にヒストグラムにしたものです。
これだけでも、立派なテキストマイニングといえます。

 

Why use

なぜ、うまいこと前処理する必要があるのか?

そもそも分かち書きされていないと、分析やら機械学習用のデータにならないという面もあります。
それに加えて重要なのが、データのnoize低減です。

教師データに使うようなデータであれば、その意味することが同じであれば、
同じ表現で統一させることができれば、それだけ揺らぎが減ります。
こうすることで、教師データとしての品質を向上させることができます

本ライブラリは、これを支援するためのものです。

 

Advertisements

source code

下記に示しておきます。

# -*- coding: utf-8 -*-
"""
Created on Wed Jul 26 13:31:40 2017
@author: boomin
"""

from __future__ import unicode_literals
import MeCab
import re
import unicodedata

class Regexp:

  def __init__(self):
    self.c = self

  def unicode_normalize(self, 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(self, 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 separateSentence(self, s):
    return s.split(" ")

  def delspace(self, s):
    for i,t in enumerate(s):
      if len(t)<1:
        s.pop(i)
    return s

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

class Mecab2:
  def __init__(self, opO="-Ochasen", dicpath="",
               target=[], mode="genkei", form="han", splitchar=" ",
               replace_rule = "",
               exclusion=["記号","BOS/EOS"]):
    # ここでクラスのメンバーが定義されているのに注意
    self.opO       = opO
    self.dicpath   = dicpath
    self.target    = target
    self.mode      = mode
    self.form      = form
    self.splitchar = splitchar
    self.exclusion = exclusion
    if len(self.dicpath)>0:
      dOps = " -d "+ self.dicpath
    else:
      dOps = ""
    if len(replace_rule)>0:
      self.replace_rule = replace_rule
    else:
      # 書き換え規則の定義
      # 基本的には単純なディクショナリ型(キー:書き換える品詞, 値:書き換え後の単語)
      # 書き換える品詞は入れ子状に記述可能(品詞→品詞細分類1→品詞細分類2→品詞細分類3の順で入れ子にしていく)
      # Default     1) "名詞"でかつ品詞細分類1が"数"ならば"[数値]"に単語を置き換えて出力
      #             2) "名詞"でかつ品詞細分類1が"固有名詞"でかつ品詞細分類2が"組織"ならば"[固有名詞_組織]"に単語を置き換えて出力
      self.replace_rule = {
         '名詞': {
          '非自立': "",
          '固有名詞': {
            '人名': ""
               }
        }
      }
    self.mecab     = MeCab.Tagger(self.opO + dOps)
    self.mecab.parse('')                # MeCab上の不具合で一度空で解析すると解消される

  def removeStoplist(self, documents, stoplist):
    #ストップワード対象文字列は、以下から取得
    #http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt
    if not isinstance(documents,str):
      return documents
    if len(stoplist)<1:
      stoplist = ["あそこ", "あたり", "あちら", "あっち", "あと", "あな", "あなた", "あれ", "いくつ", "いつ", "いま", "いや", "いろいろ", "うち", "おおまか", "おまえ", "おれ", "がい", "かく", "かたち", "かやの", "から", "がら", "きた", "くせ", "ここ", "こっち", "こと", "ごと", "こちら", "ごっちゃ", "これ", "これら", "ごろ", "さまざま", "さらい", "さん", "しかた", "しよう", "すか", "ずつ", "すね", "すべて", "ぜんぶ", "そう", "そこ", "そちら", "そっち", "そで", "それ", "それぞれ", "それなり", "たくさん", "たち", "たび", "ため", "だめ", "ちゃ", "ちゃん", "てん", "とおり", "とき", "どこ", "どこか", "ところ", "どちら", "どっか", "どっち", "どれ", "なか", "なかば", "なに", "など", "なん", "はじめ", "はず", "はるか", "ひと", "ひとつ", "ふく", "ぶり", "べつ", "へん", "ぺん", "ほう", "ほか", "まさ", "まし", "まとも", "まま", "みたい", "みつ", "みなさん", "みんな", "もと", "もの", "もん", "やつ", "よう", "よそ", "わけ", "わたし", "ハイ", "上", "中", "下", "字", "年", "月", "日", "時", "分", "秒", "週", "火", "水", "木", "金", "土", "国", "都", "道", "府", "県", "市", "区", "町", "村", "各", "第", "方", "何", "的", "度", "文", "者", "性", "体", "人", "他", "今", "部", "課", "係", "外", "類", "達", "気", "室", "口", "誰", "用", "界", "会", "首", "男", "女", "別", "話", "私", "屋", "店", "家", "場", "等", "見", "際", "観", "段", "略", "例", "系", "論", "形", "間", "地", "員", "線", "点", "書", "品", "力", "法", "感", "作", "元", "手", "数", "彼", "彼女", "子", "内", "楽", "喜", "怒", "哀", "輪", "頃", "化", "境", "俺", "奴", "高", "校", "婦", "伸", "紀", "誌", "レ", "行", "列", "事", "士", "台", "集", "様", "所", "歴", "器", "名", "情", "連", "毎", "式", "簿", "回", "匹", "個", "席", "束", "歳", "目", "通", "面", "円", "玉", "枚", "前", "後", "左", "右", "次", "先", "春", "夏", "秋", "冬", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "百", "千", "万", "億", "兆", "下記", "上記", "時間", "今回", "前回", "場合", "一つ", "年生", "自分", "ヶ所", "ヵ所", "カ所", "箇所", "ヶ月", "ヵ月", "カ月", "箇月", "名前", "本当", "確か", "時点", "全部", "関係", "近く", "方法", "我々", "違い", "多く", "扱い", "新た", "その後", "半ば", "結局", "様々", "以前", "以後", "以降", "未満", "以上", "以下", "幾つ", "毎日", "自体", "向こう", "何人", "手段", "同じ", "感じ", "てる", "いる", "なる", "れる", "する", "ある", "こと", "これ", "さん", "して", "くれる", "やる", "くださる", "そう", "せる", "した", "思う", "できる", "くる", "みる", "しまう", "それ", "ここ", "ちゃん", "くん", "て", "に", "を", "は", "の", "が", "と", "た", "し", "で", "ない", "も", "な", "い", "か", "ので", "よう"]
    removed = ""
    for doc in  documents.split():
      if not doc in stoplist:
        removed += doc + " "
    #print(removed)
    return removed.strip()


  def wakati(self, line):
    text = ''
    node = self.mecab.parseToNode(line)

    # 一単語毎に解析
    while node:

      # 形態素のノードから形態素情報を取得
      # 得られる形態素情報は基本的に以下の順番のリストになっている
      # [品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音]
      word_surface = node.surface
      word_features = node.feature.split(',')

      # 単語の置き換え規則の確認及び実行
      local_replace_rule = self.replace_rule
      for x in range(3):
        if word_features[x] in local_replace_rule:
          if isinstance(local_replace_rule[word_features[x]], dict):
            local_replace_rule = local_replace_rule[word_features[x]]
          elif isinstance(local_replace_rule[word_features[x]], str):
            word_surface = local_replace_rule[word_features[x]]
            word_features = word_features[:5] + [word_surface, word_surface, word_surface]
            break

      # 解析対象の品詞か確認
      if (not len(self.target) or \
          word_features[0] in self.target) and \
          word_features[0] not in self.exclusion:

        # 指定された形式で出力に追加
        if self.mode == 'hyousou':
          word = word_surface
        elif self.mode == 'genkei':
          #word = word_features[6]
          if word_features[6] is '*':
            word = word_surface
          else:
            word = word_features[6]
          #print(word)
        elif self.mode == 'yomi':
          word = word_features[7]

        # 未定義(辞書内で"*"と表記される)の場合は出力しない
        if word is not '*':
          text += word + self.splitchar

      node = node.next

    # 解析した行に形態素があれば出力ファイルに記述
    if text is not '':
      text = re.sub(r'\s*$', "", text)
      return text

if __name__ == '__main__': #MeCab2.pyを実行すると以下が実行される(モジュールとして読み込んだ場合は実行されない)
  import argparse

  # 実行引数の受け取り
  parser = argparse.ArgumentParser(description="MeCabで分かち書きする前処理用コード")
  parser.add_argument('target', type=str, help="前処理対象となる文字列")
  args = parser.parse_args()

  c = Regexp()
  m = Mecab2()
  print("input     : {}".format(args.target))
  regexp = c.normalize(args.target)
  print("preprosess: {}".format(regexp))
  wakati = m.wakati(regexp)
  print("wakati    : {}".format(wakati))
  print("rmv st wds: {}".format(m.removeStoplist(wakati,[])))

 
環境によっては、インデントがおかしいことがあるようなので、ここから参照してください。

Mecabのための日本語前処理例

上記のライブラリの使い方と一緒に、前処理として何をすべきなのかを紹介していきます。

Normalizing the WORDSs

Mecabを使う前に、以下のような前処理を実施することが推奨されています。

  • 半角カナ -> 全角カナ
  • 全角数字 -> 半角数字
  • 全角アルファベット -> 半角

これは一例で、ここに書いてあります

この MeCab 用のシステム辞書、Neologd の guihub に、python から使う場合の変換用のソースコードもおいてあります。
基本はパクリなのですが、これを使いやすくするために、ライブラリのように使えるようにしたものです。

 

Converting to basic form AND Excluding STOP WORDSs

Mecabで形態素解析を行うと同時に、必要な情報のみ残すことも重要です。
例えば以下のような例があります。

  • 助詞や助動詞、記号は、それ単体では意味をなさないので除外する
  • 動詞や形容詞、副詞は、基本形に変換する

一般的かどうかわかりませんが、具体的には以下のような処理をします。

  • 以下の品詞だけを残し、残りを除外する
    • 名詞
    • 動詞
    • 形容詞
    • 副詞
  • 上記の品詞を、基本形に変換する
    • 基本形がない場合は、そのままの形で残す
  • ストップワードを除去する

その時々の目的に従って、品詞の種類やmecabのシステム辞書などを、意図した形で残します。

この実装も、こちらのコードを基本的には拝借しました。
ただ、いくつか改善点があって、基本は以下の2点です。

  1. ライブラリ化して、他のpython codeから容易に呼べるようにした
  2. 基本形がない単語の場合、削除されていたが、これを元の単語のまま出力するようにした

How to use

As a single program

単体で使うときは、以下のように使います。

$ python Mecab2.py 南アルプスの天然水-Sparking*Lemon+レモン一絞り
input     : 南アルプスの天然水-Sparking*Lemon+レモン一絞り
preprosess: 南アルプスの天然水-Sparking*Lemon+レモン一絞り
wakati    : 南アルプスの天然水 Sparking Lemon レモン 一 絞る
rmv st wds: 南アルプスの天然水 Sparking Lemon レモン 絞る

入力として与えられた文字列は、全角アルファベットや記号などが含まれています。
これを規格化しものが、preprosess に表示されています。
全角の文字列が、すべて半角に統一されています。

さらに、wakatiでは、形態素解析され、かつ品詞フィルタが行われています。
具体的には、”名詞”,”動詞”,”形容詞”,”副詞” のみ、表示されていますね。

As like Library in Python code

ライブラリとして使うときは、

from Mecab2 import Mecab2, Regexp
c = Regexp()
text1 = c.normalize("南アルプスの天然水-Sparking*Lemon+レモン一絞り")
print(text1) # 南アルプスの天然水-Sparking*Lemon+レモン一絞り
m = Mecab2(target=["名詞","動詞","形容詞","副詞"])
text2 = m.wakati(text1)
print(text2) # 南アルプスの天然水 Sparking Lemon レモン 一 絞る
text3 = m.removeStoplist(text2, [])
print(text3) # 南アルプスの天然水 Sparking Lemon レモン 絞る

こちらも、同様に処理で来ていることが確認できますね。
もっといろいろ指定できたりしますが、そこはコード読んでください。

 

Reference

  1. 解析前に行うことが望ましい文字列の正規化処理
  2. MeCabとPythonで品詞を選びつつ分かち書きをしたよ
  3. 日本語のストップワードのリスト

 

Advertisements

コメントを残す