他の 100 本ノックシリーズ

言語処理 100 本ノック(第 5 章: 係り受け解析)

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

環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.4
BuildVersion:   19E266

$ python3 -V
Python 3.6.8

$ cabocha --version
cabocha of 0.69
$ cat neko.txt | cabocha -f1 > neko.txt.cabocha
$ more neko.txt.cabocha
* 0 -1D 0/0 0.000000
一      名詞,数,*,*,*,*,一,イチ,イチ
EOS
EOS
* 0 -1D 1/1 0.000000
       記号,空白,*,*,*,*, , , 
吾輩は猫である  名詞,固有名詞,一般,*,*,*,吾輩は猫である,ワガハイハネコデアル,ワガハイワネコデアル
。      記号,句点,*,*,*,*,。,。,。
EOS
* 0 2D 0/1 -1.911675
名前    名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
* 1 2D 0/0 -1.911675
まだ    副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ
* 2 -1D 0/0 0.000000
無い    形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ
。      記号,句点,*,*,*,*,。,。,。
EOS

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

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

import re
class Morph:
    """cabochaの形態素解析結果
    Args:
        line (str): e.x. '名前\t名詞,一般,*,*,*,*,名前,ナマエ,ナマエ'
    Attributes:
        surface (str): 表層形
        base (str): 基本形
        pos (str): 品詞
        pos1 (str): 品詞細分類1
    """
    def __init__(self, line):
        pattern = r"^([^,]*?)\t([^,]*?),([^,]*?)(?:,[^,]*?){4},([^,]*?)(?:(?:,[^,]*?){2})?$"
        try:
            [(self.surface, self.pos, self.pos1, self.base)] = re.findall(pattern, line)
        except:
            raise Exception(f"Invalid line pattern: \n\t{repr(line)}", )

    def __str__(self):
        return self.surface

def read_cabocha(file_name):
    with open(file_name) as f:
        lines = f.readlines()

    p = re.compile(r"^\*\ \d+\ (?:-1|\d+)D\ \d+\/\d+\ -?\d+\.\d+$")
    sentence=[]
    for line in lines:
        line = line.rstrip("\n")
        if line == "EOS":
            if sentence:
                yield sentence
            sentence = []
            continue
        elif p.match(line):
            continue
        sentence.append(Morph(line))

sentences = list(read_cabocha("neko.txt.cabocha"))
print(list(map(str, sentences[2]))) #> ['名前', 'は', 'まだ', '無い', '。']

参考: 係り受け解析レイヤの出力フォーマット

sentences ⊃ sentence = chunks ⊃ chunk = morphs ⊃ morph

memo

  • attributes は'名前\t名詞,一般,*,*,*,*,名前,ナマエ,ナマエ\n'のように(surface を除く)9 つがデフォ
  • '頸筋\t名詞,一般,*,*,*,*,*\n'この様に attributes が 7 つしかないものもある

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

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

class Chunk:
    def __init__(self, dst):
        self.morphs = self.srcs = []
        self.dst = dst

    def  __str__(self):
        return "".join(list(map(str, self.morphs)))

def read_cabocha_v2(file):
    with open(file) as f:
        sentences = f.read().split("EOS\n")

    p = re.compile(r"\*\ (\d+)\ (-1|\d+)D\ \d+\/\d+\ -?\d+\.\d+")
    for sentence in sentences:
        dep = p.findall(sentence)
        if not dep:
            continue

        chunks = []
        for (index, dst) in dep:
            c = Chunk(int(dst))
            c.srcs = [int(f) for f, t in dep if t == index]
            chunks.append(c)

        cnt = -1
        for line in sentence.splitlines():
            if p.match(line):
                cnt += 1
                continue
            else:
                chunks[cnt].morphs.append(Morph(line))
        yield chunks

sentences = list(read_cabocha_v2("neko.txt.cabocha"))
sentence = sentences[5] # 空文を除しているので6行目が該当部
for i, chunk in enumerate(sentence):
    print(f"{i}\t{chunk.dst}\t", chunk)
0	5	 吾輩 は
1	2	 ここ で
2	3	 始め て
3	4	 人間 という
4	5	 もの を
5	-1	 見 た 。

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

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

sentence = sentences[5]
for i, chunk in enumerate(sentence):
    f = lambda x: list(filter(lambda y: y.pos != "記号" , x))
    dst = i if chunk.dst == -1 else chunk.dst
    print(*f(chunk.morphs),"\t",*f(sentence[dst].morphs))
吾輩 は 	 見 た
ここ で 	 始め て
始め て 	 人間 という
人間 という 	 もの を
もの を 	 見 た
見 た 	 見 た

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

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

sentence = sentences[5]
for i, chunk in enumerate(sentence):
    if chunk.dst == -1:
        continue
    f = lambda x, y: any([m.pos==y for m in x])
    a, b = chunk.morphs, sentence[chunk.dst].morphs
    if f(a, "名詞") and f(b, "動詞"):
        f = lambda x: list(filter(lambda y: y.pos != "記号" , x))
        print(*f(a),"\t",*f(b))
吾輩 は 	 見 た
ここ で 	 始め て
もの を 	 見 た

44. 係り受け木の可視化

与えられた文の係り受け木を有向グラフとして可視化せよ.可視化には,係り受け木を DOT 言語に変換し,Graphviz を用いるとよい.また,Python から有向グラフを直接的に可視化するには,pydot を使うとよい.

from pydot import Dot, Edge, Node
graph = Dot(graph_type = 'digraph')

for i, chunk in enumerate(sentence):
    node = Node(i, label = chunk)
    graph.add_node(node)

for i, chunk in enumerate(sentence):
    if chunk.dst != -1:
        edge = Edge(i, chunk.dst)
        graph.add_edge(edge)

from PIL import Image
from io import BytesIO
img = Image.open(BytesIO(graph.create_png()))
img.show()
blog top page