LoginSignup
1
1

More than 3 years have passed since last update.

言語処理100本ノック-47:機能動詞構文のマイニング

Posted at

言語処理100本ノック 2015「第5章: 係り受け解析」47本目「機能動詞構文のマイニング」記録です。
前回ノックに加え、抽出対象がさらに複雑な条件になります。問題文を理解するだけで少し時間がかかりますし、当然解くにも時間がかかります。

参考リンク

リンク 備考
047.機能動詞構文のマイニング.ipynb 回答プログラムのGitHubリンク
素人の言語処理100本ノック:47 多くのソース部分のコピペ元
CaboCha公式 最初に見ておくCaboChaのページ

環境

CRF++とCaboChaはインストールしたのが昔すぎてインストール方法忘れました。全然更新されていないパッケージなので、環境再構築もしていません。CaboChaをWindowsで使おうと思い、挫折した記憶だけはあります。確か64bitのWindowsで使えなかった気がします(記憶が曖昧だし私の技術力の問題も多分にあるかも)。

種類 バージョン 内容
OS Ubuntu18.04.01 LTS 仮想で動かしています
pyenv 1.2.16 複数Python環境を使うことがあるのでpyenv使っています
Python 3.8.1 pyenv上でpython3.8.1を使っています
パッケージはvenvを使って管理しています
Mecab 0.996-5 apt-getでインストール
CRF++ 0.58 昔すぎてインストール方法忘れました(多分make install)
CaboCha 0.69 昔すぎてインストール方法忘れました(多分make install)

第5章: 係り受け解析

学習内容

『吾輩は猫である』に係り受け解析器CaboChaを適用し,係り受け木の操作と統語的な分析を体験します.

クラス, 係り受け解析, CaboCha, 文節, 係り受け, 格, 機能動詞構文, 係り受けパス, Graphviz

ノック内容

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

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

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

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

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

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

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

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

課題補足(「機能動詞」について)

機能動詞とは、「機能動詞・熟語動詞」によると以下を指すようです。つまり「する」みたいに名詞とくっついて「食事する」のようにしないと意味ないやつです。

動詞がその本来の意味を失い,動作名詞と結びついて動詞の意味を表す動詞を機能動詞

回答

回答プログラム 047.機能動詞構文のマイニング.ipynb

import re

# 区切り文字
separator = re.compile('\t|,')

# 係り受け
dependancy = re.compile(r'''(?:\*\s\d+\s) # キャプチャ対象外
                            (-?\d+)       # 数字(係り先)
                          ''', re.VERBOSE)

class Morph:
    def __init__(self, line):

        #タブとカンマで分割
        cols = separator.split(line)

        self.surface = cols[0] # 表層形(surface)
        self.base = cols[7]    # 基本形(base)
        self.pos = cols[1]     # 品詞(pos)
        self.pos1 = cols[2]    # 品詞細分類1(pos1)

class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.srcs = []   # 係り元文節インデックス番号のリスト
        self.dst  = dst  # 係り先文節インデックス番号

        self.phrase = ''
        self.verb = ''
        self.joshi = ''
        self.sahen = '' # サ変+を+動詞のパターン対象か否か

        for i, morph in enumerate(morphs):
            if morph.pos != '記号':
                self.phrase += morph.surface # 記号以外の場合文節作成
                self.joshi = ''  # 記号を除いた最終行の助詞を取得するため、記号以外の場合はブランク

            if morph.pos == '動詞' and self.verb == '':
                self.verb = morph.base

            if morphs[-1].pos == '助詞':
                self.joshi = morphs[-1].base

            try:
                if morph.pos1 == 'サ変接続' and \
                   morphs[i+1].surface == 'を':
                    self.sahen = morph.surface + morphs[i+1].surface
            except IndexError:
                pass

# 係り元を代入し、Chunkリストを文のリストを追加
def append_sentence(chunks, sentences):

    # 係り元を代入
    for i, chunk in enumerate(chunks):
        if chunk.dst != -1:
            chunks[chunk.dst].srcs.append(i)
    sentences.append(chunks)
    return sentences, []

morphs = []
chunks = []
sentences = []

with open('./neko.txt.cabocha') as f:

    for line in f:
        dependancies = dependancy.match(line)

        # EOSまたは係り受け解析結果でない場合
        if not (line == 'EOS\n' or dependancies):
            morphs.append(Morph(line))

        # EOSまたは係り受け解析結果で、形態素解析結果がある場合
        elif len(morphs) > 0:
            chunks.append(Chunk(morphs, dst))
            morphs = []

        # 係り受け結果の場合
        if dependancies:
            dst = int(dependancies.group(1))

        # EOSで係り受け結果がある場合
        if line == 'EOS\n' and len(chunks) > 0:
            sentences, chunks = append_sentence(chunks, sentences)

def output_file(out_file, sahen, sentence, chunk):
    # 係り元助詞のリストを作成
    sources = [[sentence[source].joshi, sentence[source].phrase] \
                for source in chunk.srcs if sentence[source].joshi != '']

    if len(sources) > 0:
        sources.sort()
        joshi = ' '.join([row[0] for row in sources])
        phrase = ' '.join([row[1] for row in sources])
        out_file.write(('{}\t{}\t{}\n'.format(sahen, joshi, phrase)))

with open('./047.result_python.txt', 'w') as out_file:
    for sentence in sentences:
        for chunk in sentence:

            if chunk.sahen != '' and \
               chunk.dst != -1 and \
               sentence[chunk.dst].verb != '':
                output_file(out_file, chunk.sahen+sentence[chunk.dst].verb, 
                            sentence, sentence[chunk.dst])
# 述語でソートして重複除去し、その件数でソート
cut --fields=1 047.result_python.txt | sort | uniq --count \
| sort --numeric-sort --reverse > 047.result_unix1.txt

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

回答解説

Chunkクラス

いつもどおり、生命線のChunkクラスを変更します。品詞細分類pos1の値が「サ変接続」の場合に次エントリを見て「を」だったらインスタンス変数sahenに両者を結合した文字列を入れています(例:返事+を)。

class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.srcs = []   # 係り元文節インデックス番号のリスト
        self.dst  = dst  # 係り先文節インデックス番号

        self.phrase = ''
        self.verb = ''
        self.joshi = ''
        self.sahen = '' # サ変+を+動詞のパターン対象か否か

        for i, morph in enumerate(morphs):
            if morph.pos != '記号':
                self.phrase += morph.surface # 記号以外の場合文節作成
                self.joshi = ''  # 記号を除いた最終行の助詞を取得するため、記号以外の場合はブランク

            if morph.pos == '動詞' and self.verb == '':
                self.verb = morph.base

            if morphs[-1].pos == '助詞':
                self.joshi = morphs[-1].base

            try:
                if morph.pos1 == 'サ変接続' and \
                   morphs[i+1].surface == 'を':
                    self.sahen = morph.surface + morphs[i+1].surface
            except IndexError:
                pass

出力部

出力部の条件分岐を変えています。

with open('./047.result_python.txt', 'w') as out_file:
    for sentence in sentences:
        for chunk in sentence:

            if chunk.sahen != '' and \
               chunk.dst != -1 and \
               sentence[chunk.dst].verb != '':
                output_file(out_file, chunk.sahen+sentence[chunk.dst].verb, 
                            sentence, sentence[chunk.dst])

出力結果(実行結果)

Python実行結果

Python Scriptを実行すると以下の結果が出力されます。

047.result_python.txt(先頭10件のみ)
決心をする と を こうと 決心を
返報をする を んで  返報を 偸んで
昼寝をする を 昼寝を
昼寝をする が を 彼が 昼寝を
迫害を加える  て を 追い廻して 迫害を
生活をする て を して 家族的生活を
話をする    を 話を
投書をする て へ を やって ほととぎすへ 投書を
話をする    に を 時に 話を
写生をする を 写生を

UNIXコマンド実行結果

UNIXコマンドを実行し、「コーパス中で頻出する述語(サ変接続名詞+を+動詞)」を出力

047.result_unix1.txt(先頭10件のみ)
     29 返事をする
     21 挨拶をする
     16 話をする
     15 真似をする
     13 喧嘩をする
      9 運動をする
      9 質問をする
      6 注意をする
      6 昼寝をする
      6 問答をする

UNIXコマンドを実行し、「コーパス中で頻出する述語と助詞パターン」を出力

047.result_unix2.txt(先頭10件のみ)
     14 返事をする と を
      9 運動をする を
      9 真似をやる を
      8 返事をする と は を
      7 喧嘩をする を
      6 話をする    に を
      6 挨拶をする と を
      5 話をする    を
      5 挨拶をする で に を
      4 質問をする て を
1
1
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
1
1