言語処理100本ノック 2015「第5章: 係り受け解析」の41本目「係り受け解析結果の読み込み(文節・係り受け)」記録です。
前回は準備運動の内容だったので、今回からが係り受けの本番です。第5章は全体的に、第4章「形態素解析」のようにパッケージを活用した短いコード、というわけにはいかず、アルゴリズムを作っていかないといけません。今回はそんなに複雑ではないですが、それでもある程度考えさせられます。
参考リンク
リンク | 備考 |
---|---|
041.係り受け解析結果の読み込み(文節・係り受け).ipynb | 回答プログラムのGitHubリンク |
素人の言語処理100本ノック:41 | 多くのソース部分のコピペ元 |
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を適用し,係り受け木の操作と統語的な分析を体験します.
ノック内容
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.
41. 係り受け解析結果の読み込み(文節・係り受け)
40に加えて,文節を表すクラスChunkを実装せよ.このクラスは形態素(Morphオブジェクト)のリスト(morphs),係り先文節インデックス番号(dst),係り元文節インデックス番号のリスト(srcs)をメンバ変数に持つこととする.さらに,入力テキストのCaboChaの解析結果を読み込み,1文をChunkオブジェクトのリストとして表現し,8文目の文節の文字列と係り先を表示せよ.第5章の残りの問題では,ここで作ったプログラムを活用せよ.
回答
回答プログラム 041.係り受け解析結果の読み込み(文節・係り受け).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 = ''.join([morph.surface for morph in morphs]) # 文節
# 係り元を代入し、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)
for i, chunk in enumerate(sentences[7]):
print('{}: {}, 係り先:{}, 係り元:{}'.format(i, chunk.phrase, chunk.dst, chunk.srcs))
回答解説
係り先取得の正規表現
係り先を取得できる正規表現を使っています。(-?\d+)
が係り先の数字を取得する部分です。正規表現に関しては記事「ゼロから覚えるPython正規表現の基本とTips」を参照ください。
正規表現を使わなくても取得できると思いますが、練習のために使っています。
# 係り受け
dependancy = re.compile(r'''(?:\*\s\d+\s) # キャプチャ対象外
(-?\d+) # 数字(係り先)
''', re.VERBOSE)
Chunkクラス
ノックの指定にかかれていないphrase
という変数も定義しています。後で出力するときに便利なので。srcs
は定義だけしておき__init__
では値の代入をしていません。
class Chunk:
def __init__(self, morphs, dst):
self.morphs = morphs
self.srcs = [] # 係り元文節インデックス番号のリスト
self.dst = dst # 係り先文節インデックス番号
self.phrase = ''.join([morph.surface for morph in morphs]) # 文節
出力結果(実行結果)
プログラム実行すると以下の結果が出力されます。
0: この, 係り先:1, 係り元:[]
1: 書生というのは, 係り先:7, 係り元:[0]
2: 時々, 係り先:4, 係り元:[]
3: 我々を, 係り先:4, 係り元:[]
4: 捕えて, 係り先:5, 係り元:[2, 3]
5: 煮て, 係り先:6, 係り元:[4]
6: 食うという, 係り先:7, 係り元:[5]
7: 話である。, 係り先:-1, 係り元:[1, 6]