yamakiy’s tech blog

技術系の記事をメインにやります。

言語処理100本ノック(第5章 40-43)

yamakiy.hatenadiary.jp github.com

新年早々質の悪い風邪をひいてました
インフルじゃなくて心底良かった(´・ω・`)

さて、本題通り100本ノック続きをやってきます

第5章: 係り受け解析

夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

まず公式にそってcabochaをいれる

いくつか注意点があったので列記

  • CRF++が必要だが、公式のはリンク切れ
  • cabochaを入れるときにライブラリエラーが発生
    • /usr/local/lib/libcabocha.so.5が読み込まれていない
      • /etc/ld.so.confに/usr/local/libを追加する
    • mecabに合わせてUTF8のmodelで入れてあげる
      • ./configure --with-charset=UTF8
      • もしオプションを忘れてmake installしたら
#!/usr/bin/python3.6
import CaboCha

if __name__ == "__main__":
    c = CaboCha.Parser()
    with open("./files/neko.txt", "r") as f:
        with open("./files/neko.txt.cabocha", "w") as f2:
            for line in f:
                f2.write(c.parse(line).toString(CaboCha.FORMAT_LATTICE))

"""
* 0 -1D 0/0 0.000000
一≫-名詞,数,*,*,*,*,一,イチ,イチ
EOS
EOS
* 0 2D 0/0 -0.764522
 ≫-記号,空白,*,*,*,*, , , 
* 1 2D 0/1 -0.764522
吾輩≫---名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ

"""

40. 係り受け解析結果の読み込み(形態素

形態素を表すクラスMorphを実装せよ.このクラスは表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をメンバ変数に持つこととする.さらに,CaboChaの解析結果(neko.txt.cabocha)を読み込み,各文をMorphオブジェクトのリストとして表現し,3文目の形態素列を表示せよ.

import re
from pprint import pprint

class Morph(object):
    surface = ""
    base = ""
    pos = ""
    pos1 = ""

    def __init__(self, line):
        self.__set_vals(line)

    def __str__(self):
        return f"{self.surface}\t{self.base}\t{self.pos}\t{self.pos1}"

    def __set_vals(self, line):
        vals = re.split(r"\t|,", line)

        self.surface = vals[0]
        self.base = vals[7]
        self.pos = vals[1]
        self.pos1 = vals[2]

class NekoCabocha(object):

    def phrase_morphs(self):
        with open("./files/neko.txt.cabocha", "r") as f:
            morphs = []
            for line in f:
                if line == "EOS\n": # 終了文字判定
                    yield morphs
                    morphs = []
                elif line[0] != "*": # * は解析開始文字
                    morphs.append(Morph(line))

if __name__ == "__main__":
    nc = NekoCabocha()
    for i, morphs in enumerate(nc.phrase_morphs()):
        if i == 2:
            print("\n".join([str(i) for i in morphs]))

"""
              記号    空白
吾輩    吾輩    名詞    代名詞
は      は      助詞    係助詞
猫      猫      名詞    一般
で      だ      助動詞  *
ある    ある    助動詞  *
。      。      記号    句点
"""

41. 係り受け解析結果の読み込み(文節・係り受け

40に加えて,文節を表すクラスChunkを実装せよ.このクラスは形態素(Morphオブジェクト)のリスト(morphs),係り先文節インデックス番号(dst),係り元文節インデックス番号のリスト(srcs)をメンバ変数に持つこととする.さらに,入力テキストのCaboChaの解析結果を読み込み,1文をChunkオブジェクトのリストとして表現し,8文目の文節の文字列と係り先を表示せよ.第5章の残りの問題では,ここで作ったプログラムを活用せよ.

import re
from pprint import pprint

class Morph(object):
    surface = ""
    base = ""
    pos = ""
    pos1 = ""

    def __init__(self, line):
        self.__set_vals(line)

    def __str__(self):
        return f"{self.surface}\t{self.base}\t{self.pos}\t{self.pos1}"

    def __set_vals(self, line):
        vals = re.split(r"\t|,", line)

        self.surface = vals[0]
        self.base = vals[7]
        self.pos = vals[1]
        self.pos1 = vals[2]

class Chunk(object):
    morphs = []
    srcs = []
    dst = -1

    def __init__(self, **kwargs):
        self.morphs = kwargs["morphs"] if "morphs" in kwargs else []
        self.dst = kwargs["dst"] if "dst" in kwargs else ""
        self.srcs = kwargs["srcs"] if "srcs" in kwargs else []

    def __str__(self):
        ret_str = f"srcs:{self.srcs}\tdst:{self.dst}\n"
        for morph in self.morphs:
            ret_str += f"{morph.surface}\t{morph.base}\t{morph.pos}\t{morph.pos1}\n"
        return ret_str

class NekoCabocha(object):

    def phrase_chunks(self):
        with open("./files/neko.txt.cabocha", "r") as f:
            chunks = {}
            for line in f:
                if line == "EOS\n": # 終了文字判定
                    yield chunks
                    chunks = {}
                elif line[0] == "*": # * は解析開始文字
                    cols = line.split(' ')
                    idx = int(cols[1])
                    dst = int(re.search(r'(.*?)D', cols[2]).group(1))
                    chunks[idx] = chunks[idx] if idx in chunks else Chunk()
                    chunks[idx].dst = dst
                    if dst != -1:
                        if dst not in chunks:
                            chunks[dst] = Chunk()
                        chunks[dst].srcs.append(idx)
                else:
                    chunks[idx].morphs.append(Morph(line))

if __name__ == "__main__":
    nc = NekoCabocha()
    for i, chunks in enumerate(nc.phrase_chunks()):
        if i == 7:
            for k in chunks:
                print(f"idx:{k}", chunks[k])
            break

"""
idx:0 srcs:[]   dst:5
吾輩    吾輩    名詞    代名詞
は      は      助詞    係助詞

idx:5 srcs:[0, 4]       dst:-1
見      見る    動詞    自立
た      た      助動詞  *
。      。      記号    句点

idx:1 srcs:[]   dst:2
ここ    ここ    名詞    代名詞
で      で      助詞    格助詞

idx:2 srcs:[1]  dst:3
始め    始める  動詞    自立
て      て      助詞    接続助詞

idx:3 srcs:[2]  dst:4
人間    人間    名詞    一般
という  という  助詞    格助詞

idx:4 srcs:[3]  dst:5
もの    もの    名詞    非自立
を      を      助詞    格助詞
"""

chunkはリストで考えるより辞書型で作成した方が、dstを先に作れるため分かりやすかった

42. 係り元と係り先の文節の表示

係り元の文節と係り先の文節のテキストをタブ区切り形式ですべて抽出せよ.ただし,句読点などの記号は出力しないようにせよ.

import re
from pprint import pprint

class Morph(object):
    surface = ""
    base = ""
    pos = ""
    pos1 = ""

    def __init__(self, line):
        self.__set_vals(line)

    def __str__(self):
        return f"{self.surface}\t{self.base}\t{self.pos}\t{self.pos1}"

    def __set_vals(self, line):
        vals = re.split(r"\t|,", line)

        self.surface = vals[0]
        self.base = vals[7]
        self.pos = vals[1]
        self.pos1 = vals[2]

class Chunk(object):
    morphs = []
    srcs = []
    dst = -1

    def __init__(self, **kwargs):
        self.morphs = kwargs["morphs"] if "morphs" in kwargs else []
        self.dst = kwargs["dst"] if "dst" in kwargs else ""
        self.srcs = kwargs["srcs"] if "srcs" in kwargs else []

    def __str__(self):
        ret_str = f"srcs:{self.srcs}\tdst:{self.dst}\n"
        for morph in self.morphs:
            ret_str += f"{morph.surface}\t{morph.base}\t{morph.pos}\t{morph.pos1}\n"
        return ret_str

class NekoCabocha(object):

    def phrase_chunks(self):
        with open("./files/neko.txt.cabocha", "r") as f:
            chunks = {}
            for line in f:
                if line == "EOS\n": # 終了文字判定
                    yield chunks
                    chunks = {}
                elif line[0] == "*": # * は解析開始文字
                    cols = line.split(' ')
                    idx = int(cols[1])
                    dst = int(re.search(r'(.*?)D', cols[2]).group(1))
                    chunks[idx] = chunks[idx] if idx in chunks else Chunk()
                    chunks[idx].dst = dst
                    if dst != -1:
                        if dst not in chunks:
                            chunks[dst] = Chunk()
                        chunks[dst].srcs.append(idx)
                else:
                    chunks[idx].morphs.append(Morph(line))

# 42. 係り元と係り先の文節の表示
def print_42():
    nc = NekoCabocha()
    for i, chunks in enumerate(nc.phrase_chunks()):
        for k in chunks:
            dst_idx = chunks[k].dst
            if dst_idx != -1:
                src = "".join([i.surface for i in chunks[k].morphs if i.pos != "記号"])
                dst = "".join([i.surface for i in chunks[dst_idx].morphs if i.pos != "記号"])
                print(f"{src}\t{dst}")

if __name__ == "__main__":
    print_42()

"""
        猫である
吾輩は  猫である
名前は  無い
まだ    無い
どこで  生れたか
生れたか        つかぬ
とんと  つかぬ
見当が  つかぬ
何でも  薄暗い
薄暗い  所で
所で    泣いて
じめじめした    所で
泣いて  記憶している
ニャーニャー    泣いて

"""

いい感じの関数名が思いつかなかったので関数名に問題番号を付けてます
こちらの方が見直すとき便利そうなので、今後はこう進める

43. 名詞を含む文節が動詞を含む文節に係るものを抽出

名詞を含む文節が,動詞を含む文節に係るとき,これらをタブ区切り形式で抽出せよ.ただし,句読点などの記号は出力しないようにせよ.

import re
from pprint import pprint

class Morph(object):
    surface = ""
    base = ""
    pos = ""
    pos1 = ""

    def __init__(self, line):
        self.__set_vals(line)

    def __str__(self):
        return f"{self.surface}\t{self.base}\t{self.pos}\t{self.pos1}"

    def __set_vals(self, line):
        vals = re.split(r"\t|,", line)

        self.surface = vals[0]
        self.base = vals[7]
        self.pos = vals[1]
        self.pos1 = vals[2]

class Chunk(object):
    morphs = []
    srcs = []
    dst = -1

    def __init__(self, **kwargs):
        self.morphs = kwargs["morphs"] if "morphs" in kwargs else []
        self.dst = kwargs["dst"] if "dst" in kwargs else ""
        self.srcs = kwargs["srcs"] if "srcs" in kwargs else []

    def __str__(self):
        ret_str = f"srcs:{self.srcs}\tdst:{self.dst}\n"
        for morph in self.morphs:
            ret_str += f"{morph.surface}\t{morph.base}\t{morph.pos}\t{morph.pos1}\n"
        return ret_str

class NekoCabocha(object):

    def phrase_chunks(self):
        with open("./files/neko.txt.cabocha", "r") as f:
            chunks = {}
            for line in f:
                if line == "EOS\n": # 終了文字判定
                    yield chunks
                    chunks = {}
                elif line[0] == "*": # * は解析開始文字
                    cols = line.split(' ')
                    idx = int(cols[1])
                    dst = int(re.search(r'(.*?)D', cols[2]).group(1))
                    chunks[idx] = chunks[idx] if idx in chunks else Chunk()
                    chunks[idx].dst = dst
                    if dst != -1:
                        if dst not in chunks:
                            chunks[dst] = Chunk()
                        chunks[dst].srcs.append(idx)
                else:
                    chunks[idx].morphs.append(Morph(line))

# 43. 名詞を含む文節が動詞を含む文節に係るものを抽出
def print_43():
    nc = NekoCabocha()
    for i, chunks in enumerate(nc.phrase_chunks()):
        for k in chunks:
            dst_idx = chunks[k].dst
            if dst_idx != -1:
                if "名詞" in [i.pos for i in chunks[k].morphs] and "動詞" in [i.pos for i in chunks[dst_idx].morphs]:
                    src = "".join([i.surface for i in chunks[k].morphs if i.pos != "記号"])
                    dst = "".join([i.surface for i in chunks[dst_idx].morphs if i.pos != "記号"])
                    print(f"{src}\t{dst}")

if __name__ == "__main__":
    print_43()