1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

言語処理100本ノック-48:名詞から根へのパスの抽出

Posted at

言語処理100本ノック 2015「第5章: 係り受け解析」48本目「名詞から根へのパスの抽出」記録です。
前回ノックと比べて少しシンプルになりました。あまり条件がなく係り先を続けて出力しているだけだからです。

参考リンク

リンク 備考
048.名詞から根へのパスの抽出.ipynb 回答プログラムのGitHubリンク
素人の言語処理100本ノック:48 多くのソース部分のコピペ元
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というファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

48. 名詞から根へのパスの抽出

文中のすべての名詞を含む文節に対し,その文節から構文木の根に至るパスを抽出せよ. ただし,構文木上のパスは以下の仕様を満たすものとする.

  • 各文節は(表層形の)形態素列で表現する
  • パスの開始文節から終了文節に至るまで,各文節の表現を"->"で連結する

「吾輩はここで始めて人間というものを見た」という文(neko.txt.cabochaの8文目)から,次のような出力が得られるはずである.

吾輩は -> 見た
ここで -> 始めて -> 人間という -> ものを -> 見た
人間という -> ものを -> 見た
ものを -> 見た

回答

回答プログラム 048.名詞から根へのパスの抽出.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.dst  = dst  # 係り先文節インデックス番号
        
        self.phrase = ''
        self.noun = False
        
        for morph in morphs:
            if morph.pos != '記号':
                self.phrase += morph.surface # 記号以外の場合文節作成
            if morph.pos == '名詞':
                self.noun = True

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.append(chunks)
            chunks = []

for i, sentence in enumerate(sentences):
    for chunk in sentence:
        if chunk.noun and chunk.dst != -1:
            line = chunk.phrase
            current_chunk = chunk
            while current_chunk.dst != -1:
                line = line + ' -> ' + sentence[current_chunk.dst].phrase
                current_chunk = sentence[current_chunk.dst]
            print(i, '\t',line)
    # 多いので制限
    if i > 10:
        break

回答解説

Chunkクラス

Chunkクラスは前回と比べてスッキリしました。名詞を含んでいるかをフラグとして持たせています。

class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.dst  = dst  # 係り先文節インデックス番号
        
        self.phrase = ''
        self.noun = False
        
        for morph in morphs:
            if morph.pos != '記号':
                self.phrase += morph.surface # 記号以外の場合文節作成
            if morph.pos == '名詞':
                self.noun = True

出力部

前回から色々とシンプルになりましたが、代わりに少し複雑にしているのが以下の出力部分です。
名詞句を含み係り先があった場合に、while句で係り先の終端まで分節をつなげています。

for i, sentence in enumerate(sentences):
    for chunk in sentence:
        if chunk.noun and chunk.dst != -1:
            line = chunk.phrase
            current_chunk = chunk
            while current_chunk.dst != -1:
                line = line + ' -> ' + sentence[current_chunk.dst].phrase
                current_chunk = sentence[current_chunk.dst]
            print(i, '\t',line)
    # 多いので制限
    if i > 10:
        break

出力結果(実行結果)

プログラム実行すると以下の結果が出力されます。

出力結果
2 	 名前は -> 無い
3 	 どこで -> 生れた -> かとんと -> つかぬ
3 	 かとんと -> つかぬ
3 	 見当が -> つかぬ
4 	 何でも -> 薄暗い -> 泣いて -> 記憶している
4 	 した所で -> 泣いて -> 記憶している
4 	 いた事だけは -> 記憶している
5 	 吾輩は -> 見た
5 	 ここで -> 始めて -> 人間という -> ものを -> 見た
5 	 人間という -> ものを -> 見た
5 	 ものを -> 見た
6 	 あとで -> 聞くと -> そうだ
6 	 それは -> そうだ
6 	 書生という -> 人間中で -> 種族であった -> そうだ
6 	 人間中で -> 種族であった -> そうだ
6 	 一番 -> 獰悪な -> 種族であった -> そうだ
6 	 獰悪な -> 種族であった -> そうだ
6 	 種族であった -> そうだ
7 	 書生というのは -> 話である
7 	 我々を -> 捕えて -> 煮て -> 食うという -> 話である
8 	 当時は -> なかったから -> 思わなかった
8 	 何という -> 考も -> なかったから -> 思わなかった
8 	 考も -> なかったから -> 思わなかった
9 	 彼の -> 掌に -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
9 	 掌に -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
9 	 スーと -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
9 	 時 -> フワフワした -> 感じが -> あったばかりである
9 	 感じが -> あったばかりである
10 	 掌の -> 上で -> 落ちついて -> 見たのが -> 人間という -> ものの -> 見始であろう
10 	 上で -> 落ちついて -> 見たのが -> 人間という -> ものの -> 見始であろう
10 	 書生の -> 顔を -> 見たのが -> 人間という -> ものの -> 見始であろう
10 	 顔を -> 見たのが -> 人間という -> ものの -> 見始であろう
10 	 見たのが -> 人間という -> ものの -> 見始であろう
10 	 人間という -> ものの -> 見始であろう
10 	 ものの -> 見始であろう
11 	 時 -> ものだと -> 思った -> 感じが -> 残っている
11 	 妙な -> ものだと -> 思った -> 感じが -> 残っている
11 	 ものだと -> 思った -> 感じが -> 残っている
11 	 感じが -> 残っている
11 	 今でも -> 残っている
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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?