言語処理100本ノック 2015 第5章 40〜44

言語処理100本ノック 2015」の「第5章: 係り受け解析」、40〜44。

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

まずneko.txt.cabochaの作成。以降の課題で形態素解析が必要なので、以下のオプションで出力したものをneko.txt.cabochaとして保存しました。

cabocha -f1 neko.txt > neko.txt.cabocha

出力結果はこんな感じ。

* 0 -1D 0/0 0.000000
一	名詞,数,*,*,*,*,一,イチ,イチ
EOS
EOS
* 0 2D 0/0 -0.764522
 	記号,空白,*,*,*,*, , , 
* 1 2D 0/1 -0.764522
吾輩	名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
* 2 -1D 0/2 0.000000
猫	名詞,一般,*,*,*,*,猫,ネコ,ネコ
で	助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
ある	助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル
。	記号,句点,*,*,*,*,。,。,。
EOS
…

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

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

処理としては4章の内容が流用可能でクラスを使うようになったくらい。

import argparse


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('infile')
    args = parser.parse_args()

    with open(args.infile, mode='r') as f:
        for line_num, s in enumerate(read_sentences(f)):
            if line_num == 2:
                print('\n'.join([str(m) for m in s]))
                break


def read_sentences(lines):
    morphemes = []

    for line in lines:
        if line.rstrip() == 'EOS':
            yield morphemes

            morphemes = []
        elif line[0] != '*':
            morphemes.append(Morph(line))


class Morph:
    def __init__(self, line):
        t1 = line.split('\t')
        t2 = t1[1].split(',')
        self.surface = t1[0]
        self.base = t2[6]
        self.pos = t2[0]
        self.pos1 = t2[1]

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


if __name__ == '__main__':
    main()

出力結果はこんな感じ。

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

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

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

40の課題に加えてChunkクラスを作り、read_sentences関数を変更。

import argparse


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('infile')
    args = parser.parse_args()

    with open(args.infile, mode='r') as f:
        for line_num, sentence in enumerate(read_sentences(f)):
            if line_num == 7:
                for i, c in enumerate(sentence):
                    print(f'{i}\t{c.get_surface()}\tdst:{c.dst}')
                break


def read_sentences(lines):
    chunks = []
    chunk_lines = []

    for line in lines:
        if line[0] == '*':
            if len(chunk_lines) > 0:
                chunks.append(Chunk(chunk_lines))

            chunk_lines = [line]
        elif line.rstrip() == 'EOS':
            if len(chunk_lines) > 0:
                chunks.append(Chunk(chunk_lines))

            for i, c in enumerate(chunks):
                if c.dst != -1:
                    chunks[c.dst].srcs.append(i)

            yield chunks

            chunks = []
            chunk_lines = []
        else:
            chunk_lines.append(line)


class Morph:
    def __init__(self, line):
        t1 = line.split('\t')
        t2 = t1[1].split(',')
        self.surface = t1[0]
        self.base = t2[6]
        self.pos = t2[0]
        self.pos1 = t2[1]

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


class Chunk:
    def __init__(self, lines):
        self.srcs = []
        t = lines[0].split()
        self.dst = int(t[2][0:-1])
        self.morphs = [Morph(l) for l in lines[1:]]

    def __repr__(self):
        return (f'{self.dst}\n' + str(self.srcs) + '\n' +
                '\n'.join([str(m) for m in self.morphs]) + '\n---\n')

    def get_surface(self):
        return ''.join([m.surface for m in self.morphs])


if __name__ == '__main__':
    main()

出力はこんな感じ。

0       吾輩は  dst:5
1       ここで  dst:2
2       始めて  dst:3
3       人間という      dst:4
4       ものを  dst:5
5       見た。  dst:-1

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

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

41の課題に「第5章の残りの問題では,ここで作ったプログラムを活用せよ」の記載があったとおり41をベースにします。
まず「句読点などの記号は出力しないようにせよ」の条件を満たすため、Chunkクラスにnomarkメソッドとget_surface_nomarkメソッドを追加。

class Chunk:
    def __init__(self, lines):
        self.srcs = []
        t = lines[0].split()
        self.dst = int(t[2][0:-1])
        self.morphs = [Morph(l) for l in lines[1:]]

    def __repr__(self):
        return (f'{self.dst}\n' + str(self.srcs) + '\n' +
                '\n'.join([str(m) for m in self.morphs]) + '\n---\n')

    def get_surface(self):
        return ''.join([m.surface for m in self.morphs])

    def nomark(self):
        return [m for m in self.morphs if m.pos != '記号']

    def get_surface_nomark(self):
        return ''.join([m.surface for m in self.nomark()])

これを使う側のmain関数も以下のように変更。

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('infile')
    args = parser.parse_args()

    with open(args.infile, mode='r') as f:
        for s in read_sentences(f):
            for c in s:
                if c.dst >= 0 and c.nomark() and s[c.dst].nomark():
                    print(
                        f'{c.get_surface_nomark()}\t' +
                        f'{s[c.dst].get_surface_nomark()}')

出力はこんな感じ。

吾輩は  猫である
名前は  無い
まだ    無い
どこで  生れたか
生れたか        つかぬ
とんと  つかぬ
見当が  つかぬ
何でも  薄暗い
薄暗い  所で
じめじめした    所で
…

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

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

Chunkクラスに「名詞を含む文節」「動詞を含む文節」を判定するためにhasVerbメソッド、hasNounメソッドを追加。

class Chunk:
    def __init__(self, lines):
        self.srcs = []
        t = lines[0].split()
        self.dst = int(t[2][0:-1])
        self.morphs = [Morph(l) for l in lines[1:]]

    def __repr__(self):
        return (f'{self.dst}\n' + str(self.srcs) + '\n' +
                '\n'.join([str(m) for m in self.morphs]) + '\n---\n')

    def get_surface(self):
        return ''.join([m.surface for m in self.morphs])

    def nomark(self):
        return [m for m in self.morphs if m.pos != '記号']

    def get_surface_nomark(self):
        return ''.join([m.surface for m in self.nomark()])

    def hasVerb(self):
        return '動詞' in [m.pos for m in self.nomark()]

    def hasNoun(self):
        return '名詞' in [m.pos for m in self.nomark()]

使用するmain関数側も修正。

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('infile')
    args = parser.parse_args()

    with open(args.infile, mode='r') as f:
        for s in read_sentences(f):
            for c in s:
                if c.dst >= 0 and c.hasNoun() and s[c.dst].hasVerb():
                    print(
                        f'{c.get_surface_nomark()}\t' +
                        f'{s[c.dst].get_surface_nomark()}')

出力はこんな感じ。

どこで  生れたか
見当が  つかぬ
所で    泣いて
ニャーニャー    泣いて
いた事だけは    記憶している
吾輩は  見た
ここで  始めて
ものを  見た
あとで  聞くと
我々を  捕えて
…

44. 係り受け木の可視化

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

main関数のみの変更で対応。「Pythonから有向グラフを直接的に可視化するには,pydotを使うとよい」とはあるものの、以前の記事でgraphvizを使って有向グラフ作ったことがあるので、その時のノウハウを使って実装。

「DataLab.を使ってサイアーラインを可視化してみる(その2)」で取得した種牡馬情報を可視化してみます。 約1000頭の情報を...

任意の入力からグラフ化する必要があるので、直接CaboChaを使えるようにpythonバインディングを以下の記事を参考にインストール。

# TL;DR - 自然言語処理には欠かせないライブラリであるMeCabとCaboChaをbrewでサクッと導入する - ついでにpythonから使えるようにしちゃう # 環境 - macOS X 10.13.3 High Si...
import CaboCha
from graphviz import Digraph


def main():
    g = Digraph(format='png')

    cabocha = CaboCha.Parser()
    cabochastr = cabocha.parse(input()).toString(CaboCha.FORMAT_LATTICE)
    lines = cabochastr.splitlines()

    s = list(read_sentences(lines))[0]
    for i, c in enumerate(s):
        if c.nomark():
            g.node(str(i), label=c.get_surface_nomark())

            if c.dst >= 0 and s[c.dst].nomark():
                g.edge(str(i), str(c.dst))

    g.render('ch05-44')

出力はこんな感じ。

スポンサーリンク

フォローする

スポンサーリンク