自然言語処理プログラムを自作していく


Pythonの勉強がてら日本語用の自然言語処理ライブラリを見よう見まねで作ってみます。
途中で根本的に間違う可能性もありますが、失敗も含めて勉強の過程を生々しく書いていけたらと思います。

まずは形態素解析器の実装を目指していきます。

形態素解析器を作るまでの道のり

形態素解析器を作るまでに、だいたい次のステップを踏んでいきます。

1.日本語の語彙を集めた辞書を作る
2.品詞同士の接続可能性を集めた接続表を作る
3.語彙辞書と接続表を使って形態素解析器を作る

それぞれの段階にはもっと広大で深淵な要素が含まれていると思いますが、まずはざっくり全体像を掴みながら作っていく方針です。

1.語彙辞書を作る

コンピュータが与えられた文字列の中から形態素候補となり得る単語を識別するために、あらかじめ世間で使われている単語を集めた辞書を作っておいて、それをコンピュータに入力してあげる必要があります。(ちなみに、この文章では「形態素」と「単語」は同じ意味で使ってます。)
世の単語を手作業で集めて辞書を作る気力はないので、ここはフリーの公開コーパスをありがたく使わせて頂きましょう。
今回は下記サイトで配布されているChaSen形式のコーパスjeita_genpaku.tar.bz2、jeita_aozora.tar.bz2)を単語のソースとして使わせて頂きます。
http://lilyx.net/nltk-japanese-corpus/#jeitac

コーパスって単語ごとに分割されてはいるけど基本は元の文章そのままなので、コーパス中に同じ単語が重複して現れていて、このままだと語彙の辞書としては不向きです。なので、コーパスから単語を取り出して重複を無くしたものを語彙辞書として別に作成します。
そのためには、まずChaSen形式のコーパスを読み込むリーダーの作成から始めます。

ChaSenコーパスリーダーの実装

ChaSenコーパスファイルの中をのぞくと、例えば次のような形式で書かれています。

私	ワタシ	私	名詞-代名詞-一般
の	ノ	の	助詞-連体化
テーマ	テーマ	テーマ	名詞-一般
について	ニツイテ	について	助詞-格助詞-連語
お話し	オハナシ	お話し	名詞-サ変接続
する	スル	する	動詞-自立	サ変・スル	基本形

コーパスファイル内では1行につき1単語の情報が書かれています。ChaSenコーパスのフォーマットに関する公式な仕様は見つけられなかったですが、実物を観察すると一つの単語を表すフォーマットは以下のようになってるみたいです。

見出し形[Tab]読み[Tab]原形[Tab]品詞1(-品詞2)(-品詞3)(-品詞4)([Tab]活用種別[Tab]活用形)

小括弧でくくった項目は、単語によってあったりなかったりする項目です。つまりタブで区切られた項目が4つまたは6つで構成されていて、品詞の部分はさらにハイフン区切りで1〜4段階に細分化されて記述されるということになります。
このChaSen形式の単語を表すChasenWordクラスを作ってみます。
開発環境のPythonバージョンは3.4.2です。

class ChasenWord(object):
    def __init__(self, raw_str):
        self.lemma = ''
        self.pron = ''
        self.base = ''
        self.pos = []
        self.conj_type = ''
        self.conj_form = ''
        self.is_bos = False
        self.is_eos = False

        tokens = raw_str.split('\t')
        if len(tokens) not in [1, 4, 6]:
            raise Exception('invalid corpus line : ' + raw_str)
        self._extract(tokens)
    
    def _extract(self, tokens):
        if tokens[0] == 'BOS':
            self.is_bos = True
            self.lemma = self.pron = self.base = 'BOS'
            return
        if tokens[0] == 'EOS':
            self.is_eos = True
            self.lemma = self.pron = self.base = 'EOS'
            return

        self.lemma = tokens[0]
        self.pron = tokens[1]
        self.base = tokens[2]
        parts = tokens[3].split('-')
        for p in parts:
            self.pos.append(p)
        if len(tokens) == 6:
            self.conj_type = tokens[4]
            self.conj_form = tokens[5]
        else:
            self.conj_type = ''
            self.conj_form = ''

ChasenWordクラスは、ChaSen形式の1単語分の文字列リテラルでコンストラクトし、中で文字列を解析して構成要素をメンバに保持します。ChaSen形式の単語は基本的にはタブで区切られた4また6個の値で構成されていますが、特別な場合としてセンテンスの終わりを示すEOSがあり、この単語(?)は1要素だけを持つようです。なのでChasenWordでは1単語の要素数が1、4、6個であることを想定し、それ以外はエラーにしてます。
動作確認すると以下のようになります。

>>> from chasen_word import ChasenWord
>>> chasen_str = '立ち向かう\tタチムカウ\t立ち向かう\t動詞-自立\t五段・ワ行促音便\t基本形'
>>> cw = ChasenWord(chasen_str)
>>> cw.lemma
'立ち向かう'
>>> cw.pron
'タチムカウ'
>>> cw.base
'立ち向かう'
>>> cw.pos
['動詞', '自立']
>>> cw.conj_type
'五段・ワ行促音便'
>>> cw.conj_form
'基本形'

1単語ずつ生成するのも使いづらいので、ファイル単位でコーパスを読み込んでそこに含まれるすべての単語からChasenWordを生成するChasenCorpusReaderも作っておきます。

import os
from nlang.corpus.chasen.chasen_word_b import ChasenWord

class ChasenCorpusReader(object):
    def __init__(self, file_path):
        self.words = None
        self._read(os.path.expanduser(file_path))

    def _read(self, file_path):
        with open(file_path, 'r') as f:
            self.words = [ChasenWord(line[:-1]) for line in f.readlines()]

これで最低限コーパスファイルを読み込んでデータを取れるようになったので、次はこれを使って辞書を作っていきたいと思います。