2018年05月10日

言語処理100本ノックでPython入門 #41 - 分節と係り受け

  
今日は、言語処理100本ノック 2015の問題41です。

■ 問題


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

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


■ Pythonのコード
import re

class Morph:
    def __init__(self, surface, base, pos, pos1):
        self.surface = surface
        self.base = base
        self.pos = pos
        self.pos1 = pos1

    def toList(self):
        return [self.surface, self.base, self.pos, self.pos1]

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

    def print(self):
        print(self.number)
        print([x.toList() for x in self.morphs])
        print(self.dst, self.srcs)
        print()

def analyze():
    article = []
    sentence = []
    chunk = None
    with open('chap05/neko.txt.cabocha', 'r', encoding='utf8') as fin:
        for line in fin:
            words = re.split(r'\t|,|\n| ', line)
            if line[0] == '*':
                num = int(words[1])
                destNo = int(words[2].rstrip('D'))
                chunk = Chunk(num, destNo)
                sentence.append(chunk)
            elif words[0] == 'EOS':
                if sentence:
                    for index, c in enumerate(sentence, 0):
                        sentence[c.dst].srcs.append(index)
                    article.append(sentence)
                sentence = []
            else:
                chunk.morphs.append(Morph(
                    words[0],
                    words[7],
                    words[1],
                    words[2],
                ))
    return article

def main():
    article = analyze()
    for c in article[8]:
        c.print()

if __name__ == '__main__':
    main()

■ どう解いたか 

この問題を解くのに、文章全体をarticleという変数で表しています。article変数は、以下のような構造になっています。

  • articleは、複数のsentenceから成る -> sentenceのリスト 
  • sentenceは、複数のchunckから成る -> chunckのリスト 
  • chunckは、複数のMorhpから成る -> Morhpのリストをメンバーに持つ

Chunk.srcsの値(係り元文節インデックス番号のリスト)をどう設定するかが悩みましたが、'EOS'の行が現れたら、その直前の文 - つまりsentence変数(リスト)を走査して、各Chunkのsrcsを設定するようにしました。

問題には指定されていませんが、Chunkクラスに勝手に、
self.number = number
を追加しました。このメンバーは表示用なので、無くても問題の本質部分のanalyze関数には影響ないです。

コードの途中に出てくるNoneは、C#のnullのようなものという理解。
chunk = None

Morphクラス、Chunkクラス、analyze関数は、問題42以降使い回すことになると思います。

■ 結果

問題文が求めている表示の形式と違うような気もしますが、本質的な部分ではないと思うので、これで良しとします。

1行目が分節の番号、2行目が分節、3行目が係り先の番号と[]の中は、係り元の番号となっています。
それが、分節の数だけ続きます。最後は、係り先はないので、-1となっています。

0
[['しかし', 'しかし', '接続詞', '*']]
9 []

1
[['その', 'その', '連体詞', '*']]
2 []

2
[['当時', '当時', '名詞', '副詞可能'], ['は', 'は', '助詞', '係助詞']]
5 [1]

3
[['何', '何', '名詞', '代名詞'], ['という', 'という', '助詞', '格助詞']]
4 []

4
[['考', '考', '名詞', '一般'], ['も', 'も', '助詞', '係助詞']]
5 [3]

5
[['なかっ', 'ない', '形容詞', '自立'], ['た', 'た', '助動詞', '*'], ['から', 'から', '助詞', '接続助詞']]
9 [2, 4]

6
[['別段', '別段', '副詞', '助詞類接続']]
7 []

7
[['恐し', '恐い', '形容詞', '自立']]
9 [6]

8
[['いとも', 'いとも', '副詞', '一般']]
9 []

9
[['思わ', '思う', '動詞', '自立'], ['なかっ', 'ない', '助動詞', '*'], ['た', 'た', '助動詞', '*'], ['。', '。', '記号', '句点']]
-1 [0, 5, 7, 8, 9]