ナード戦隊データマン

機械学習と自然言語処理についてのブログ

ntlkでツリーバンクからPCFGを自動生成

PCFGとは、文脈自由文法の各ルールに対して確率を付与したものです。ここでは、Keyaki treebankというコーパスからPCFGを生成します。

Keyaki treebankとは

Keyaki Treebankは、日本語の一貫した記述文法を具体化し、さまざまな文法現象を検索できるようにするための、解析されたコーパスです。

以下から詳細を見れます。 http://www.compling.jp/keyaki/

以下はダウンロードページです。 https://github.com/ajb129/KeyakiTreebank/releases

Keyaki Extractorで訓練データ生成

以下に、keyaki_extractor.pyというスクリプトを置きました。 https://github.com/sugiyamath/keyaki_extractor/blob/master/keyaki_extractor.py

これをインポートして以下のように訓練データを生成します。

import os
import keyaki_extractor as ke
from collections import defaultdict
from tqdm import tqdm
import re

IDREG = re.compile(r"\(ID .+?\)")
STARTREG = re.compile(r"^\(")

def main(path, fpart=".psd", outfile="out3.txt"):
    print("File listing")
    files = [f for f in os.listdir(path) if fpart in f]
    print(files)
    out = []

    print("Data extracting")
    for f in tqdm([os.path.join(path, g) for g in files]):
        data = ke.load_data(f)
        for d in data:
            if d == "":
                continue
            tmp = ''.join(
                list(ke.readiter(d, include_content=True, exclude=["ID"])))
            tmp = re.sub(IDREG, "", tmp)
            tmp = re.sub(STARTREG, "(S", tmp)
            out.append(tmp)

    print("Data writing")
    with open(outfile, "w") as f:
        f.write('\n'.join(out))
    
if __name__ == "__main__":
    main("../../../keyaki/KeyakiTreebank/treebank/")

なお、main関数にKeyaki treebankのパスを指定しています。treebank内にはclosedファイルを消すか、あるいはclosedに対応したコーパスを購入して復元してください。

PCFGの訓練

以下に、cfg_toolsというスクリプト集を置いています。 https://github.com/sugiyamath/cfg_experiments/tree/master/cfg_tools

こちらの一部をインポートして、以下の訓練スクリプトを回します:

import sys
sys.path.append("./cfg_tools/3/")

import learn_pcfg as lp
import pickle as pkl
import nltk


def load_data(infile="./out3.txt"):
    out = []
    with open(infile) as f:
        for line in f:
            line = line.strip()
            out.append(nltk.tree.Tree.fromstring(line))
    return out


if __name__ == "__main__":
    trees = load_data()
    model = lp.learn_trees(trees, collapse=True, markov_order=True)
    with open("model.pkl", "wb") as f:
        pkl.dump(model, f)

訓練済みPCFGの公開

以下は、7z圧縮された訓練済みPCFGです(ただし、nltkを使っています)。 https://github.com/sugiyamath/cfg_experiments/blob/master/model/model.7z

この文法ルールをソートして文字列として出力したものが以下です。 https://github.com/sugiyamath/cfg_experiments/blob/master/data/keyaki_pcfg.txt

任意の文の構文解析

PCFGを使えば、構文解析ができます。nltkに用意されているいくつかのパーサを使うことができます。

訓練済みモデルを用いてパースするには以下のスクリプトを実行します:

import sys
sys.path.append("./cfg_tools/3/")

import learn_pcfg as lp
import pickle
import nltk
from nltk.parse.viterbi import ViterbiParser


def load_model(model_file="./model.pkl"):
    with open(model_file, "rb") as f:
        model = pickle.load(f)
    return model

def eval_model(sent, grammar, tokenize, outfile="example_parse.txt"):
    target = tokenize(sent)
    #parser = ViterbiParser(grammar)
    #parser = InsideChartParser(grammar, beam_size=50)
    for x in lp.prob_parse(grammar, target, n=1):
        return x
    

if __name__ == "__main__":
    import time
    import MeCab
    sent = "子供が泳いでいる写真がかかっていた。"
    tagger = MeCab.Tagger("-Owakati")
    start = time.time()
    grammar = load_model()
    print(time.time() - start)
    start = time.time()
    print(eval_model(sent, grammar, tagger.parse))
    print(time.time() - start)

[出力]

(S
  (IP-IMP
    (PP
      (NP
        (IP-EMB
          (NP;*SBJ* (N 子供) (P が))
          (VB 泳い)
          (P で)
          (VB2 いる))
        (N 写真))
      (P が))
    (VB かかっ)
    (P て)
    (VB2 い)
    (AX た))
  (PU 。)) (p=4.51538e-41)

nltkにはViterbiParserというパーサが用意されていますが、遅いのでPCFGの訓練に利用したモジュール内に定義したprob_parse関数を用いてパースしています。

補足

このスクリプトMeCabをちゃっかり使っていますが、Keyakiの単位に合わせるために、以下のスクリプトをまず回しています。

# coding: utf-8
import re
from tqdm import tqdm

regex = re.compile(r"'(.+?)'")
with open("./keyaki_pcfg.txt") as f:
    data = f.read()
    
out =[x.group(1) for x in tqdm(regex.finditer(data))]
with open("foo.csv", "w") as f:
    for o in out:
        if "," in o:
            print("HO")
            continue
        t = "{},,,1,名詞,一般,*,*,*,*,None,None,None,None\n".format(o)
        f.write(t)

これは、ユーザ定義辞書にとりあえず語彙ルールを突っ込むというものです。そのユーザ辞書は以下に置きました: https://github.com/sugiyamath/cfg_experiments/tree/master/data/mecab_userdic

参考