MeCabをブーストさせよう

# はじめに

MeCabとは日本語の形態素解析器をいいます。ここで、形態素というのは言語で意味を持つ最小単位のことです。分割された単語をベクトル化したり、各語彙の頻度を調べたりするために、最小単位に分割するモチベーションが生じます。

そもそもなぜ、形態素解析なんかやるの？っていう動機については、http://qiita.com/Hironsan/items/2466fe0f344115aff177 とかに書かれている通り、(上記の記事では、単語の分割が形態素解析に当たります)、分割された単語をベクトル化したり、各語彙の頻度を調べたりするためです。今回は、MeCabを用いて、できるだけ、精度高く分かち書きできるように頑張ります。[^prec]

[^prec]: 精度高くというのは、複合語なども分割されずに一語として、認識されることを表します。

追記) もう一つのMecabをブーストさせよう(Google Search Console編: https://qiita.com/knknkn1162/items/a73d22e516680e7744ba )の記事を2017.12に書いたので、
こちらもどうぞ。

## 動機
MeCab をちょっと触ってみたが、もうちょっと新語や特殊語彙を踏まえて、分かち書きしたい。

## 前提知識

MeCabのインストールとか済ませておくこと。とりあえずの使い方はわかる程度とします。
あ、環境はMac OS Sierraです[^mac]。

[^mac]: CentOSなどのLinux系とほとんど同じなのですが、mecabのパス関連がちょっと違っていて、macだと、`/usr/local/Cellar/mecab/0.996/libexec/mecab/mecab-dict-index`となります。

# 辞書追加

手段として、

+ 公開されている拡張辞書を用いる(`mecab-ipadic-neologd`が最もポピュラー)

+ 自身で新語を加える

という2つを紹介します。前者の方は https://github.com/neologd/mecab-ipadic-neologd を見れば大体のことは書いてあるので、さらっと流して、後半に重点を置きたいです。

## `mecab-ipadic-neologd`を使う

まず、みんなご存知、`mecab-ipadic-neologd`を入れます。


```bash
# 新語辞書 mecab-ipadic-neologd の追加(2回目以降は) 辞書更新
./bin/install-mecab-ipadic-neologd -n -a 
```

```bash
# terminal上では、これで使える。
mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd
```

pythonで用いる場合は、形態素解析の便利クラス(Morphクラス)を以下に定義して、使用するのが良いと思います：

```python
import MeCab
class Morph(object):
    kinds = None
    log_stamp = 1000

    @classmethod
    def set_log(cls, num):
        cls.log_stamp = num

    @classmethod
    def set_kind(cls, *args):
        cls.kinds = args
        return cls

    @classmethod
    def reset_kind(cls):
        cls.kinds = None
        return cls

    def set_file(self, file, up_to=100000000):
        fi = codecs.open(file, 'r', 'utf-8', 'ignore')
        self.lines = fi
        return self

    def set_sentence(self, sentence):
        self.lines = sentence.split('\n')
        return self

    def set_chasen(self, s):
        import MeCab
        self.chasen = MeCab.Tagger(s)
        return self

    def set_wakati(self, s):
        import MeCab
        self.wakati = MeCab.Tagger(s)
        return self

    def __init__(self, dic_path=None):
        try:
            self.chasen = MeCab.Tagger('-Ochasen -d {}'.format(dic_path))
            self.wakati = MeCab.Tagger('-Owakati -d {}'.format(dic_path))
        except:
            self.chasen = MeCab.Tagger('-Ochasen')
            self.wakati = MeCab.Tagger('-Owakati')


    def wakatigaki(self):
        res = ''
        for line in self.lines:
            res += self.wakati.parse(line)
        return res

    def extract(self, feature=True):
        feature_flag = feature
        '''return type of list'''
        tagger = self.chasen
        for i, line in enumerate(self.lines):
            line = line.strip()
            if (i + 1) % self.log_stamp == 0:
                print('line {}'.format(i + 1))
            # 最後はEOSなので、読み飛ばす
            chunks = tagger.parse(line).splitlines()[:-1]

            for idx, chunk in enumerate(chunks):
                try:
                    # #表層形\t読み\t原型\t品詞なので、(原型、品詞)のみとり出す。
                    _surface, _yomi, origin, feature = chunk.split('\t')[:4]
                except:
                    import traceback
                    print('×', end="/")
                    continue
                origin = origin.lower()
                if Morph.kinds is None:
                    if feature_flag:
                        yield (origin, feature.split('-')[0])
                    elif not feature_flag:
                        yield origin
                    continue
                for kind in Morph.kinds:
                    if feature.startswith(kind):
                        if feature_flag:
                            yield (origin, kind)
                        elif not feature_flag:
                            yield origin
                        break
    return 0
```

を用意しておいて、

```python
# txtを事前に用意しておく
mp = Morph(dic_path = path)
mp.set_sentence(txt)
print(list(mp.extract()))
```

みたいな感じで使えば良いです。

## サイトから新語を作成する

今回のメインです。`mecab-ipadic-neologd`じゃ、まだまだ求める精度に達していないんじゃ〜、って時に以下の方法により、更なる高精度を実現します：

1. 所要のサイトをスクレイピング
1. GCP Natural Language APIで新語を取得
1.  新語をchasen形式のCSVに保存
1. CSVをdicに変換 
1. mecab設定ファイルに前記の`dic`形式のファイルのpathをユーザー辞書として登録しておく。
1. 上記で設定が完了したので、あとはMeCabを普通に使う。

という感じ。1番目については、スクレイピングの技術なので、[Pythonクローリング&スクレイピング](https://www.amazon.co.jp/dp/4774183679/ref=sr_1_1?ie=UTF8&qid=1495290988&sr=8-1&keywords=スクレイピング)とかの本を読んでいただくとして[^diffbot]、2番目から説明していきますね😇

[^diffbot]: 実は、生のhtmlだと、広告とかの除去判定が大変だと思います。自分(の会社)ではdiffbotを使っています。AIが自動で記事にあたる部分のみを抽出してくれるらしいです。また、別記事で取り上げたいです。

### GCP Natural Language APIで新語を取得

+ 新語を拾うために、今回は、GCP Natural Language APIを用いて、スクレイピングで得たtxtから、語彙(entity)を取り出し、機械的に新語を抜き出します[^gcp_nlp]：

[^gcp_nlp]: 導入方法については、雑で手前味噌ですが、 http://qiita.com/knknkn1162/items/0b7528118cc4ef54a648 を参照してください。


```python
from google.cloud import language
class GCPNaturalLanguage(object):
    def __init__(self, upper=10000):
        # Instantiates a client
        self.client = language.Client()
        self.upper = upper

    def get_entity(self, text):
        length = len(text)

        if length > self.upper:
            print("{} .. too long".format(length))
            return {}

        document = self.client.document_from_text(text, language='ja')

        # Detects the sentiment of the text
        res = document.analyze_entities()
        print("{} characters => done!".format(len(text)))
        dic = {}
        for entity in res.entities:

            for m in entity.mentions:
                dic.update({m.text.begin_offset: m.text.content})

        return dic
```

```python
import GCPNaturalLanguage
# txtはスクレイピングによって得られた文字列
gcn = GCPNaturalLanguage()
dic = gcn.get_entity(txt) # 単語の位置がkey, 単語がvalueにあたる。
words = dic.values()
```

### 新語をchasen形式のCSVに保存

#### 頻度表の作成

新語かどうかを判定するために、頻度表を作ることで、頻度表に存在しない語彙を抜き出します。[^hist]

[^hist]: 直接、mecab内部のcsvファイルから参照してもいいのだが、頻度表は分かち書きのステップ以降であると非常に便利なtableなので、今回はこの手段を取ることにした。


分かち書きしたものの語彙の頻度表を作成していきます(MeCabのシステム辞書のみ(ユーザー辞書は追加していない)である点に注意すること)
今回はwikipediaのコーパスを用いて、http://eyepodtouch.net/?p=77 の手順で`txt`ファイルを作成し、 https://github.com/neologd/mecab-ipadic-neologd/wiki/Regexp を参考に、ゴミを除去and 正規化します。そのあと、頻度表を作成します：

```python
# 頻度表
import collections
import Morph
def create_word_hist(file, path):
    mp = Morph(dic_path = path)
    mp.set_file(file)
    # mp.extract()は分かち書きされた(origin, kind)のgeneratorを返す
    counts = collections.defaultdict(int)
    for tup in mp.extract():
        counts[tup] += 1
  return counts
```

#### CSV出力

サイトのスクレイピングtxtデータから、chasen形式の.csvファイルを作成します[^chasen]：

[^chasen]: formatとかについては、http://taku910.github.io/mecab/dic.html に書いてあります。

```python
import os
from pandas import Series, DataFrame

# wordsはmecabの辞書に登録されていないword
def save_chasen_csv(words, file=None):
    chasen_mapping = \
        ['表層形', '左文脈ID', '右文脈ID', 'コスト', '品詞',
         '品詞細分類1', '品詞細分類2', '品詞細分類3',
         '活用形', '活用型', '原形', '読み', '発音']

    word_series_list = []
    for w in words:
        word_series = Series([None] * len(chasen_mapping), index=chasen_mapping)
        word_series[['表層形', '品詞', '品詞細分類1', '原形']] = [w, '名詞', '一般', w]
        word_series[['品詞細分類2', '品詞細分類3', '活用形', '活用型']] = '*'
        word_series_list.append(word_series)

    new_word_df = DataFrame(word_series_list)
    if file is not None:
        os.makedirs(os.path.dirname(file), exist_ok=True)
        new_word_df.to_csv(file, index=False, header=False)

    return new_word_df
```

以上のお膳立てをした上で、下記のコードを実行します：


```python
# 頻度表
# wiki_file .. http://eyepodtouch.net/?p=77 を参照して、作っておく
# path .. "/usr/local/lib/mecab/dic/mecab-ipadic-neologd" などMeCabのシステム辞書のpathを入れる
## wikipediaのfileは2.5Gくらいあるので、時間かかる
counts = create_word_hist(wiki_file, path)
# 与えられたwordsから、出現する語彙countsを除去したものを返す
## この関数は各自用意してください
new_words = extract_nonexist_words(words, counts)
# new_wordsはmecabの辞書に登録されていないwordのlist
save_chasen_csv(new_words, file = "/home/foo/bar/test_dic.csv")
```

これで、CSVファイル(`"/home/foo/bar/test_dic.csv"`)ができました。ちゃんとchase形式になってます。

### ユーザー辞書の作成

このあと、自動コスト推定、`dic`ファイルを経て、`mecab`の設定ファイルにユーザー辞書を登録します。
主に、 https://taku910.github.io/mecab/dic.html を参照しました。

準備として、modelファイルを作っておきます。

+ http://taku910.github.io/mecab/#download から
ダウンロード > MeCab 用の辞書 > IPA辞書 の項目をたどっていき、ダウンロードのurlをコピー( https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7MWVlSDBCSXZMTXM )し、以下のように処理します：


```bash
## http://qiita.com/wakisuke/items/d15b5defc1aad61cc910 を参考にした
# IPA辞書のutf-8に
% bzip2 -d mecab-ipadic-2.7.0-20070801.model.bz2
% vi ./mecab-ipadic-2.7.0-20070801.model #６行目を「charset: utf-8」に書き換えてください
% nkf -w --overwrite ./mecab-ipadic-2.7.0-20070801.model #文字コードをutf-8へ変換
```

```bash
# chasen形式の新語のcsvが存在しているディレクトリに飛ぶ
cd /home/foo/bar
# GCP natural language APIで拾った語彙のコスト自動推定
## macの場合(linuxの場合 /usr/libexec/mecab/mecab-dict-index)
/usr/local/Cellar/mecab/0.996/libexec/mecab/mecab-dict-index -m ./mecab-ipadic-2.7.0-20070801.model \
-d ./mecab-ipadic -f utf-8 -t utf-8 \
-a test_dic.csv -u new_test_dic.csv
./mecab-ipadic-2.7.0-20070801.model is not a binary model. reopen it as text mode...
reading new_test_dic.csv ... 
done!

# 再び辞書を作成
/usr/local/Cellar/mecab/0.996/libexec/mecab/mecab-dict-index -m ./mecab-ipadic-2.7.0-20070801.model \
-d ./mecab-ipadic \
-f utf-8 -t utf-8 \
-u ./site.dic \
./new_test_dic.csv # 新規に追加するcsv
```

`site.dic`が完成品です。

### ユーザー辞書の登録

いよいよ、mecabの設定ファイルに`site.dic`を追加します。

```bash
vim /usr/local/etc/mecabrc

# ユーザ辞書への追加
# 適当な行に付け足す
userdic = /home/foo/bar/site.dic
```

----

あとは、普通に使えばよいです：

```bash
# terminal上では、これで使える。
mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd
```

とするか、頻度表の作成で登場した、Morphクラスを`dic_path="/usr/local/lib/mecab/dic/mecab-ipadic-neologd"`つきで初期化すれば、新語を踏まえた上での分かち書きがきます。

一見遠回りなことをしていますが、これで、スクレイピング対象サイトに関して、新語を適宜追加することで、より精度の高い分かち書きが実現できます。

## 終わりに

今回、`mecab-ipadic-neologd`よりも高精度の分かち書きについて紹介したのですが、学習フェーズにおいて、この手段はあまり効果がありません。(そもそも、複合語は頻出度がかなり低くなる傾向にあるため、のちにword2vecでベクトル化を行なったとしても、その複合語がベクトル空間上に正しく表されるのかかなり怪しいから)
なので、評価フェーズの前処理として、上記で述べたテクニックを目標達成の一部として用いるのが良いでしょう。

それでは、よい分かち書きライフを👋