言語処理100本ノック-40:係り受け解析結果の読み込み(形態素)


言語処理100本ノック 2015「第5章: 係り受け解析」40本目「係り受け解析結果の読み込み(形態素)」記録です。
これから始まる第5章は、全体的にアルゴリズムを組むのが面倒で時間がかかり、言語処理100本ノックの最初の鬼門のような気がします。
今回は準備運動のようなものでたいして難しくないです。100本ノックでクラスを初めて使うのがせいぜい真新しい内容でしょうか。

参考リンク

リンク 備考
040.係り受け解析結果の読み込み(形態素).ipynb 回答プログラムのGitHubリンク
素人の言語処理100本ノック:40 多くのソース部分のコピペ元
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というファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

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

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

課題補足(「係り受け」について)

「係り受け」は文節間の関係です。以前記事「【お遊び】シンカリオンのトンデモメールを構文解析」で少しやったのですが、こんな文書でも

関係性を明確にすることができます。

回答

回答前提

まずは、CaboChaで係り受け解析をします。

cabocha -f1 ../04.形態素解析/neko.txt -o neko.txt.cabocha

以下のような実行結果です。MeCabの結果に係り受け情報が付加されています。1行目の* 0 -1D 0/0 0.000000の部分が係り受け情報で3文字目の0が分節番号で、その後の-1が係り先を示しています。今回は-1と係り先なしなので、例が悪いですね。

neko.txt.cabochaの一部抜粋
* 0 -1D 0/0 0.000000
一 名詞,数,*,*,*,*,一,イチ,イチ
EOS
EOS
* 0 -1D 1/1 0.000000
  記号,空白,*,*,*,*, , , 
吾輩は猫である   名詞,固有名詞,一般,*,*,*,吾輩は猫である,ワガハイハネコデアル,ワガハイワネコデアル
。 記号,句点,*,*,*,*,。,。,。
EOS
* 0 2D 0/1 -1.911675
名前  名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
* 1 2D 0/0 -1.911675
まだ  副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ
* 2 -1D 0/0 0.000000
無い  形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ
。 記号,句点,*,*,*,*,。,。,。
EOS
EOS
* 0 1D 1/2 1.504358
  記号,空白,*,*,*,*, , , 
どこ  名詞,代名詞,一般,*,*,*,どこ,ドコ,ドコ
で 助詞,格助詞,一般,*,*,*,で,デ,デ
* 1 2D 0/1 1.076607
生れ  動詞,自立,*,*,一段,連用形,生れる,ウマレ,ウマレ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
* 2 4D 0/1 -0.197109
かとん   名詞,一般,*,*,*,*,火遁,カトン,カトン
と 助詞,格助詞,一般,*,*,*,と,ト,ト
* 3 4D 0/1 -0.197109
見当  名詞,サ変接続,*,*,*,*,見当,ケントウ,ケントー
が 助詞,格助詞,一般,*,*,*,が,ガ,ガ
* 4 -1D 0/1 0.000000
つか  動詞,自立,*,*,五段・カ行イ音便,未然形,つく,ツカ,ツカ
ぬ 助動詞,*,*,*,特殊・ヌ,基本形,ぬ,ヌ,ヌ
。 記号,句点,*,*,*,*,。,。,。
EOS

回答プログラム 040.係り受け解析結果の読み込み(形態素).ipynb

でもって、本題のPythonプログラムです。

import re

morphs = []
sentences = []

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

# 除外行
exclude = re.compile(r'''EOS\n      # EOS, 改行コード
                         |          # OR
                         \*\s\d+\s  # '*, 空白, 数字1つ以上, 空白
                       ''', 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)

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

    for line in f:
        if not exclude.match(line):
            morphs.append(Morph(line))
        if   line == 'EOS\n' \
         and len(morphs) > 0:
            sentences.append(morphs)
            morphs = []

for sentence in sentences[2]:
    print(sentence.__dict__)

回答解説

正規表現

第2章で習った正規表現を練習と思い使っています。separatorは形態素解析結果に対する区切り文字、excludeはEOSと係り受け結果を除外するための正規表現です。正規表現に関しては記事「ゼロから覚えるPython正規表現の基本とTips」を参照ください。

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

# 除外行
exclude = re.compile(r'''EOS\n      # EOS, 改行コード
                         |          # OR
                         \*\s\d+\s  # '*, 空白, 数字1つ以上, 空白
                       ''', re.VERBOSE)

クラス

100本ノックで始めてでてくるクラスです。__init__は初回時に呼ばれるコンストラクタです。形態素解析結果の行全体を受け取り、タブ/カンマで区切ってクラス変数に格納しています。

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)

クラス変数の出力

__dict__とすることでクラス変数を辞書型で出力してくれます。知らなかったのですが便利ですね。

for sentence in sentences[2]:
    print(sentence.__dict__)

出力結果(実行結果)

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

出力結果
{'surface': '名前', 'base': '名前', 'pos': '名詞', 'pos1': '一般'}
{'surface': 'は', 'base': 'は', 'pos': '助詞', 'pos1': '係助詞'}
{'surface': 'まだ', 'base': 'まだ', 'pos': '副詞', 'pos1': '助詞類接続'}
{'surface': '無い', 'base': '無い', 'pos': '形容詞', 'pos1': '自立'}
{'surface': '。', 'base': '。', 'pos': '記号', 'pos1': '句点'}