LoginSignup
2
2

More than 5 years have passed since last update.

素人の言語処理100本ノック:47

Last updated at Posted at 2016-12-07

言語処理100本ノック 2015の挑戦記録です。環境はUbuntu 16.04 LTS + Python 3.5.2 :: Anaconda 4.1.1 (64-bit)です。過去のノックの一覧はこちらからどうぞ。

第5章: 係り受け解析

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

47. 機能動詞構文のマイニング

動詞のヲ格にサ変接続名詞が入っている場合のみに着目したい.46のプログラムを以下の仕様を満たすように改変せよ.

  • 「サ変接続名詞+を(助詞)」で構成される文節が動詞に係る場合のみを対象とする
  • 述語は「サ変接続名詞+を+動詞の基本形」とし,文節中に複数の動詞があるときは,最左の動詞を用いる
  • 述語に係る助詞(文節)が複数あるときは,すべての助詞をスペース区切りで辞書順に並べる
  • 述語に係る文節が複数ある場合は,すべての項をスペース区切りで並べる(助詞の並び順と揃えよ)

例えば「別段くるにも及ばんさと、主人は手紙に返事をする。」という文から,以下の出力が得られるはずである.

返事をする      と に は        及ばんさと 手紙に 主人は

このプログラムの出力をファイルに保存し,以下の事項をUNIXコマンドを用いて確認せよ.

  • コーパス中で頻出する述語(サ変接続名詞+を+動詞)
  • コーパス中で頻出する述語と助詞パターン

出来上がったコード:

main.py
# coding: utf-8
import CaboCha
import re

fname = 'neko.txt'
fname_parsed = 'neko.txt.cabocha'
fname_result = 'result.txt'


def parse_neko():
    '''「吾輩は猫である」を係り受け解析
    「吾輩は猫である」(neko.txt)を係り受け解析してneko.txt.cabochaに保存する
    '''
    with open(fname) as data_file, \
            open(fname_parsed, mode='w') as out_file:

        cabocha = CaboCha.Parser()
        for line in data_file:
            out_file.write(
                cabocha.parse(line).toString(CaboCha.FORMAT_LATTICE)
            )


class Morph:
    '''
    形態素クラス
    表層形(surface)、基本形(base)、品詞(pos)、品詞細分類1(pos1)を
    メンバー変数に持つ
    '''
    def __init__(self, surface, base, pos, pos1):
        '''初期化'''
        self.surface = surface
        self.base = base
        self.pos = pos
        self.pos1 = pos1

    def __str__(self):
        '''オブジェクトの文字列表現'''
        return 'surface[{}]\tbase[{}]\tpos[{}]\tpos1[{}]'\
            .format(self.surface, self.base, self.pos, self.pos1)


class Chunk:
    '''
    文節クラス
    形態素(Morphオブジェクト)のリスト(morphs)、係り先文節インデックス番号(dst)、
    係り元文節インデックス番号のリスト(srcs)をメンバー変数に持つ
    '''

    def __init__(self):
        '''初期化'''
        self.morphs = []
        self.srcs = []
        self.dst = -1

    def __str__(self):
        '''オブジェクトの文字列表現'''
        surface = ''
        for morph in self.morphs:
            surface += morph.surface
        return '{}\tsrcs{}\tdst[{}]'.format(surface, self.srcs, self.dst)

    def normalized_surface(self):
        '''句読点などの記号を除いた表層形'''
        result = ''
        for morph in self.morphs:
            if morph.pos != '記号':
                result += morph.surface
        return result

    def chk_pos(self, pos):
        '''指定した品詞(pos)を含むかチェックする

        戻り値:
        品詞(pos)を含む場合はTrue
        '''
        for morph in self.morphs:
            if morph.pos == pos:
                return True
        return False

    def get_morphs_by_pos(self, pos, pos1=''):
        '''指定した品詞(pos)、品詞細分類1(pos1)の形態素のリストを返す
        pos1の指定がない場合はposのみで判定する

        戻り値:
        形態素(morph)のリスト、該当形態素がない場合は空のリスト
        '''
        if len(pos1) > 0:
            return [res for res in self.morphs
                    if (res.pos == pos) and (res.pos1 == pos1)]
        else:
            return [res for res in self.morphs if res.pos == pos]

    def get_kaku_prt(self):
        '''助詞を1つ返す
        複数ある場合は格助詞を優先し、最後の助詞を返す。

        戻り値:
        助詞、ない場合は空文字列
        '''
        prts = self.get_morphs_by_pos('助詞')
        if len(prts) > 1:

            # 2つ以上助詞がある場合は、格助詞を優先
            kaku_prts = self.get_morphs_by_pos('助詞', '格助詞')
            if len(kaku_prts) > 0:
                prts = kaku_prts

        if len(prts) > 0:
            return prts[-1].surface     # 最後を返す
        else:
            return ''

    def get_sahen_wo(self):
        '''「サ変接続名詞+を」を含無場合は、その部分の表層形を返す

        戻り値:
        「サ変接続名詞+を」の文字列、なければ空文字列
        '''
        for i, morph in enumerate(self.morphs[0:-1]):

            if (morph.pos == '名詞') \
                    and (morph.pos1 == 'サ変接続') \
                    and (self.morphs[i + 1].pos == '助詞') \
                    and (self.morphs[i + 1].surface == 'を'):
                return morph.surface + self.morphs[i + 1].surface

        return ''


def neco_lines():
    '''「吾輩は猫である」の係り受け解析結果のジェネレータ
    「吾輩は猫である」の係り受け解析結果を順次読み込んで、
    1文ずつChunkクラスのリストを返す

    戻り値:
    1文のChunkクラスのリスト
    '''
    with open(fname_parsed) as file_parsed:

        chunks = dict()     # idxをkeyにChunkを格納
        idx = -1

        for line in file_parsed:

            # 1文の終了判定
            if line == 'EOS\n':

                # Chunkのリストを返す
                if len(chunks) > 0:

                    # chunksをkeyでソートし、valueのみ取り出し
                    sorted_tuple = sorted(chunks.items(), key=lambda x: x[0])
                    yield list(zip(*sorted_tuple))[1]
                    chunks.clear()

                else:
                    yield []

            # 先頭が*の行は係り受け解析結果なので、Chunkを作成
            elif line[0] == '*':

                # Chunkのインデックス番号と係り先のインデックス番号取得
                cols = line.split(' ')
                idx = int(cols[1])
                dst = int(re.search(r'(.*?)D', cols[2]).group(1))

                # Chunkを生成(なければ)し、係り先のインデックス番号セット
                if idx not in chunks:
                    chunks[idx] = Chunk()
                chunks[idx].dst = dst

                # 係り先のChunkを生成(なければ)し、係り元インデックス番号追加
                if dst != -1:
                    if dst not in chunks:
                        chunks[dst] = Chunk()
                    chunks[dst].srcs.append(idx)

            # それ以外の行は形態素解析結果なので、Morphを作りChunkに追加
            else:

                # 表層形はtab区切り、それ以外は','区切りでバラす
                cols = line.split('\t')
                res_cols = cols[1].split(',')

                # Morph作成、リストに追加
                chunks[idx].morphs.append(
                    Morph(
                        cols[0],        # surface
                        res_cols[6],    # base
                        res_cols[0],    # pos
                        res_cols[1]     # pos1
                    )
                )

        raise StopIteration


# 係り受け解析
parse_neko()

# 結果ファイル作成
with open(fname_result, mode='w') as out_file:

    # 1文ずつリスト作成
    for chunks in neco_lines():

        # chunkを列挙
        for chunk in chunks:

            # 動詞を含むかチェック
            verbs = chunk.get_morphs_by_pos('動詞')
            if len(verbs) < 1:
                continue

            # 係り元に助詞を含むchunkを列挙
            chunks_include_prt = []
            for src in chunk.srcs:
                if len(chunks[src].get_kaku_prt()) > 0:
                    chunks_include_prt.append(chunks[src])
            if len(chunks_include_prt) < 1:
                continue

            # 係り元に「サ変接続名詞+を(助詞)」があるかチェック
            sahen_wo = ''
            for chunk_src in chunks_include_prt:
                sahen_wo = chunk_src.get_sahen_wo()
                if len(sahen_wo) > 0:
                    chunk_remove = chunk_src
                    break
            if len(sahen_wo) < 1:
                continue

            # 「サ変接続名詞+を(助詞)」は述語として動詞と一緒に出力するので係り元からは除外
            chunks_include_prt.remove(chunk_remove)

            # chunkを助詞の辞書順でソート
            chunks_include_prt.sort(key=lambda x: x.get_kaku_prt())

            # 出力
            out_file.write('{}\t{}\t{}\n'.format(
                sahen_wo + verbs[0].base,   # サ変接続名詞+を+最左の動詞の基本系
                ' '.join([chunk.get_kaku_prt() \
                        for chunk in chunks_include_prt]),      # 助詞
                ' '.join([chunk.normalized_surface() \
                        for chunk in chunks_include_prt])       # 項
            ))

実行結果:

以下、結果の先頭部分です。

result.txt(先頭部分)
決心をする と あるこうと
返報をしてやる   んで  偸んで
昼寝をする     
昼寝をする が 彼が
迫害を加える  て 追い廻して
生活をする が て を 等猫族が 完くして 愛を
話をする        
投書をする て へ やって ほととぎすへ
話をする    に 時に
写生をする     
昼寝をする て 出て
彩色を見る     
欠伸をする から て て  なったから して 押し出して
報道をする に 耳に
前後を忘れる  に 心に
挨拶をする     
御馳走を食う  と 見ると
問答をする     
雑談をする ながら は   寝転びながら 黒は
自慢をする     
呼吸を飲み込む   から  なってから
思案を定める  と は 若くはないと 吾輩は
御馳走をあるく   って て  猟って なって
御馳走を食う      
放蕩をする     
放蕩をする が ものだからが
放蕩をする より  云うよりも
放蕩をする     
写生を力む に従って    忠告に従って
写生をする     

結果全体はGitHubにアップしています。

UNIXコマンド確認用のシェルスクリプト

test.sh
#!/bin/sh

# 述語でソートして重複除去し、その件数でソート
cut --fields=1 result.txt | sort | uniq --count | sort --numeric-sort --reverse > "predicate.txt"

# 述語と助詞でソートして重複除去し、その件数でソート
cut --fields=1,2 result.txt | sort | uniq --count | sort --numeric-sort --reverse > "predicate_Particle.txt"

cutで切り出してuniqsortで集計、ソートしています。いずれのコマンドも問題19などで出てきたものです。

結果の確認:

コーパス中で頻出する述語(サ変接続名詞+を+動詞)の先頭部分です。

predicate.txt(先頭部分)
     30 返事をする
     21 挨拶をする
     15 話をする
     14 真似をする
     12 喧嘩をする
      8 質問をする
      7 運動をする
      6 注意をする
      6 昼寝をする
      6 邪魔をする
      5 話を聞く
      5 問答をする
      5 病気をする
      5 相談をする
      5 質問をかける
      5 辞儀をする
      4 放蕩をする
      4 戦争をする
      4 散歩をする
      4 降参をする
      4 欠伸をする
      4 休養を要する
      4 演説をする
      4 いたずらをする

コーパス中で頻出する述語と助詞パターンの先頭部分です。

predicate_particle.txt(先頭部分)
      8 真似をする 
      7 返事をする と
      6 運動をする 
      5 喧嘩をする 
      4 話を聞く    
      4 話をする    
      4 返事をする と は
      4 挨拶をする と
      4 挨拶をする から
      3 返事をする 
      3 質問をかける  と は
      3 喧嘩をする と
      2 問答をする 
      2 放蕩をする 
      2 返事をする から と
      2 平均を破る 
      2 病気をする 
      2 同情を表す に
      2 注意を惹く 
      2 注意をする 
      2 深入りをする  
      2 写生をする 
      2 散歩をする 
      2 講義をする 
      2 行動をとる 
      2 工夫をする 
      2 交際をする 
      2 御無沙汰をする   
      2 休養を要する  は
      2 議論をする て
      2 学問をする 
      2 覚悟をする と
      2 運動をやる 
      2 安心を得る が
      2 挨拶をする と も
      2 挨拶をする で
      2 挨拶をする 
      2 いたずらをする   

結果全体はGitHubにアップしています。

述語に係る助詞が1つしかない場合の扱い

Chunkクラスにget_sahen_wo()を追加しました。文節の中に「サ変接続名詞+を(助詞)」がある場合に、その表層形を返します。それ以外のクラスや関数は前問のままです。

この問題で悩んだのは、「サ変接続名詞+を(助詞)」の文節以外に、動詞を含む文節に係る文節がなかった場合の扱いです。除去すべきなのか良くわからなかったので、「サ変接続名詞+を(助詞)」の文節しか動詞に係る文節がないものも残してあります。タブ区切りの2カラム目と3カラム目が空文字列になっているのはこのパターンです。

機能動詞とは

動詞だけでは内容がわからず、名詞と組み合わせて内容がわかるようになる動詞だそうです。確かに「する」だけでは内容が分かりませんが「返事を」と組み合わせると内容が分かりますね。このような動詞のことです。

 
48本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。


実行結果には、100本ノックで用いるコーパス・データで配布されているデータの一部が含まれます。この第5章で用いているデータは青空文庫で公開されている夏目漱石の長編小説『吾輩は猫である』が元になっています。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2