言語処理100本ノック2020 第5章 係り受け解析

はじめに

言語処理100本ノック2020

言語処理100本ノックは東北大学が公開している自然言語処理の問題集です。

とても良質なコンテンツで企業の研修や勉強会で使われています。

そんな言語処理100本ノックが2020年に改定されてました。昨今の状況を鑑みて、深層ニューラルネットワークに関する問題が追加されました。(その他にも細かい変更があります)

この記事では、言語処理100本ノック2020にPythonで取り組んでいきます。

他にも色々な解法があると思うので、一つの解答例としてご活用ください!

全100問の解説に戻る

準備

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

cabochaをインストールしていない方はインストールしておいて下さい。Macの場合、下記のコマンドでOKです。

1
brew install cabocha

リンクからneko.txtをダウンロードして、下記のコマンドを実行します。

1
cabocha -f1 < neko.txt > neko.txt.cabocha

これで準備完了です。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Morph:
def __init__(self, surface, base, pos, pos1):
self.surface = surface
self.base = base
self.pos = pos
self.pos1 = pos1
def show(self):
print (self.surface, self.base, self.pos, self.pos1)
path = 'neko.txt.cabocha'
with open(path) as f:
text = f.read().split('\n')
result = []
morphs = []
for line in text[:-1]:
if line == 'EOS':
result.append(morphs)
morphs = []
elif line[0] == '*':
continue
else:
ls = line.split('\t')
d = {}
tmp = ls[1].split(',')
morph = Morph(ls[0],tmp[6],tmp[0],tmp[1])
morphs.append(morph)
for morphs in result[2]:
morphs.show()

EOSや解析結果(先頭が*から始まる)などの特殊な行に注意しましょう。

41. 係り受け解析結果の読み込み(文節・係り受け)

40に加えて,文節を表すクラスChunkを実装せよ.このクラスは形態素(Morphオブジェクト)のリスト(morphs),係り先文節インデックス番号(dst),係り元文節インデックス番号のリスト(srcs)をメンバ変数に持つこととする.さらに,入力テキストのCaboChaの解析結果を読み込み,1文をChunkオブジェクトのリストとして表現し,8文目の文節の文字列と係り先を表示せよ.第5章の残りの問題では,ここで作ったプログラムを活用せよ.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class Morph:
def __init__(self, surface, base, pos, pos1):
self.surface = surface
self.base = base
self.pos = pos
self.pos1 = pos1
def show(self):
print (self.surface, self.base, self.pos, self.pos1)

class Chunk:
def __init__(self, sentence_id, chunk_id, dst, srcs):
self.sentence_id = sentence_id
self.chunk_id = chunk_id
self.morphs = []
self.dst = dst
self.srcs = srcs
def show_morphs(self):
morphs = ''
for morph in self.morphs:
morphs += morph.surface
print ("morphs:",morphs)
def show_chunk_id(self):
print ("==========")
print ("chunk_id:",self.chunk_id)
def show_sentence_id(self):
if (self.chunk_id == 0):
print ("====================")
print ("sentence_id:",self.sentence_id)
def show_dst(self):
print ("dst:",self.dst)
def show_srcs(self):
print ("srcs:",self.srcs[self.chunk_id])

path = 'neko.txt.cabocha'
with open(path) as f:
text = f.read().split('\n')
result = []
morphs = []
chunks = []
srcs = [[]]
chunk = None
sentence_id = 0
chunk_id = 0

for line in text[:-1]:
if line == 'EOS':
result.append(morphs)
morphs = []
sentence_id += 1
chunk_id = 0
srcs = [[]]

elif line[0] == '*':
if chunk:
chunks.append(chunk)
dst = int(line.split()[2][:-1])
diff = dst + 1- len(srcs)
ex = [[] for _ in range(diff)]
srcs.extend(ex)
if dst!=-1:
srcs[dst].append(chunk_id)
chunk = Chunk(sentence_id, chunk_id, dst, srcs)
chunk_id += 1

else:
ls = line.split('\t')
d = {}
tmp = ls[1].split(',')
morph = Morph(ls[0],tmp[6],tmp[0],tmp[1])
morphs.append(morph)
chunk.morphs.append(morph)

else:
chunks.append(chunk)
for i,chunk in enumerate(chunks):
if chunk.sentence_id == 7:
chunk.show_sentence_id()
chunk.show_chunk_id()
chunk.show_dst()
chunk.show_srcs()
chunk.show_morphs()

係り先文節インデックス番号(dst)は解析結果として得られているのでそのまま利用すれば良いですが、
係り元文節インデックス番号のリスト(srcs)は自前で用意する必要があります。

そこでdstの集計と並行して、srcs[dst]を作成します。この時点では文節の数が不明なので、必要に応じてsrcsをextendします。

42. 係り元と係り先の文節の表示

係り元の文節と係り先の文節のテキストをタブ区切り形式ですべて抽出せよ.ただし,句読点などの記号は出力しないようにせよ.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import itertools
class Morph:
def __init__(self, surface, base, pos, pos1):
self.surface = surface
self.base = base
self.pos = pos
self.pos1 = pos1
def show(self):
print (self.surface, self.base, self.pos, self.pos1)

class Chunk:
def __init__(self, sentence_id, chunk_id, dst, srcs):
self.sentence_id = sentence_id
self.chunk_id = chunk_id
self.morphs = []
self.dst = dst
self.srcs = srcs
def show_morphs(self):
morphs = ''
for morph in self.morphs:
morphs += morph.surface
print ("morphs:",morphs)
def show_chunk_id(self):
print ("==========")
print ("chunk_id:",self.chunk_id)
def show_sentence_id(self):
if (self.chunk_id == 0):
print ("====================")
print ("sentence_id:",self.sentence_id)
def show_dst(self):
print ("dst:",self.dst)
def show_srcs(self):
print ("srcs:",self.srcs[self.chunk_id])

path = 'neko.txt.cabocha'
with open(path) as f:
text = f.read().split('\n')
result = []
morphs = []
chunks = []
srcs = [[]]
chunk = None
sentence_id = 0
chunk_id = 0

for line in text[:-1]:
if line == 'EOS':
result.append(morphs)
morphs = []
sentence_id += 1
chunk_id = 0
srcs = [[]]

elif line[0] == '*':
if chunk:
chunks.append(chunk)
dst = int(line.split()[2][:-1])
diff = dst + 1- len(srcs)
ex = [[] for _ in range(diff)]
srcs.extend(ex)
if dst!=-1:
srcs[dst].append(chunk_id)
chunk = Chunk(sentence_id, chunk_id, dst, srcs)
chunk_id += 1

else:
ls = line.split('\t')
d = {}
tmp = ls[1].split(',')
morph = Morph(ls[0],tmp[6],tmp[0],tmp[1])
morphs.append(morph)
chunk.morphs.append(morph)

else:
chunks.append(chunk)

sentences = [[] for _ in range(len(chunks))]
for chunk in chunks:
morphs = ''
for morph in chunk.morphs:
morphs += morph.surface
sentences[chunk.sentence_id].append(morphs)

dsts = [[] for _ in range(len(chunks))]
for chunk in chunks:
dst = sentences[chunk.sentence_id][chunk.dst]
dsts[chunk.sentence_id].append(dst)
sentences = list(itertools.chain.from_iterable(sentences))
dsts = list(itertools.chain.from_iterable(dsts))
for i, (s, d) in enumerate(zip(sentences, dsts)):
s = s.replace(" ","").replace("。","").replace("、","")
d = d.replace(" ","").replace("。","").replace("、","")
if s==d or s=='' or d=='':
continue
print (s,d)

itertools.chain.from_iterableを使って二次元リストを一次元リストに変換している。

43. 名詞を含む文節が動詞を含む文節に係るものを抽出

名詞を含む文節が,動詞を含む文節に係るとき,これらをタブ区切り形式で抽出せよ.ただし,句読点などの記号は出力しないようにせよ.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import itertools
class Morph:
def __init__(self, surface, base, pos, pos1):
self.surface = surface
self.base = base
self.pos = pos
self.pos1 = pos1
def show(self):
print (self.surface, self.base, self.pos, self.pos1)

class Chunk:
def __init__(self, sentence_id, chunk_id, dst, srcs):
self.sentence_id = sentence_id
self.chunk_id = chunk_id
self.morphs = []
self.dst = dst
self.srcs = srcs
self.has_noun = False
self.has_verb = False
self.surfaces = ''
def show_morphs(self):
morphs = ''
for morph in self.morphs:
morphs += morph.surface
print ("morphs:",morphs)
def show_chunk_id(self):
print ("==========")
print ("chunk_id:",self.chunk_id)
def show_sentence_id(self):
if (self.chunk_id == 0):
print ("====================")
print ("sentence_id:",self.sentence_id)
def show_dst(self):
print ("dst:",self.dst)
def show_srcs(self):
print ("srcs:",self.srcs[self.chunk_id])

path = 'neko.txt.cabocha'
with open(path) as f:
text = f.read().split('\n')
result = []
morphs = []
chunks = []
srcs = [[]]
chunk = None
sentence_id = 0
chunk_id = 0

for line in text[:-1]:
if line == 'EOS':
result.append(morphs)
morphs = []
sentence_id += 1
chunk_id = 0
srcs = [[]]

elif line[0] == '*':
if chunk:
chunks.append(chunk)
dst = int(line.split()[2][:-1])
diff = dst + 1- len(srcs)
ex = [[] for _ in range(diff)]
srcs.extend(ex)
if dst!=-1:
srcs[dst].append(chunk_id)
chunk = Chunk(sentence_id, chunk_id, dst, srcs)
chunk_id += 1

else:
ls = line.split('\t')
d = {}
tmp = ls[1].split(',')
morph = Morph(ls[0],tmp[6],tmp[0],tmp[1])
morphs.append(morph)
chunk.morphs.append(morph)

else:
chunks.append(chunk)

sentences = [[] for _ in range(len(chunks))]
for chunk in chunks:
for morph in chunk.morphs:
chunk.surfaces += morph.surface
if morph.pos == '動詞':
chunk.has_verb = True
elif morph.pos == '名詞':
chunk.has_noun = True


sentences[chunk.sentence_id].append(chunk)

dsts = [[] for _ in range(len(chunks))]
for chunk in chunks:
dst = sentences[chunk.sentence_id][chunk.dst]
dsts[chunk.sentence_id].append(dst)
sentences = list(itertools.chain.from_iterable(sentences))
dsts = list(itertools.chain.from_iterable(dsts))
for i, (sentence, dst) in enumerate(zip(sentences, dsts)):
if sentence.has_noun and dst.has_verb:
s = sentence.surfaces
d = dst.surfaces

s = s.replace(" ","").replace("。","").replace("、","")
d = d.replace(" ","").replace("。","").replace("、","")
if s==d or s=='' or d=='':
continue
print (s,d)

クラスChunkに以下のインスタンス変数を追加しました。

1
2
3
self.has_noun = False
self.has_verb = False
self.surfaces = ''

44. 係り受け木の可視化

与えられた文の係り受け木を有向グラフとして可視化せよ.可視化には,係り受け木をDOT言語に変換し,Graphvizを用いるとよい.また,Pythonから有向グラフを直接的に可視化するには,pydotを使うとよい

準備

今回はpydotを利用した。必要があればインストールする。

1
pip install pydot

pydotgraphvizのインターフェイスを提供するものなので、graphviz本体も必要があればインストールする。

1
brew install graphviz

pydotの基本

公式の情報が見当たりませんでしたので、こちらを参考にさせていただきました。

グラフの構築

1
graph = pydot.Dot()
1
graph = pydot.Dot(graph_type='digraph') # 有向グラフ
1
graph = pydot.Dot(graph_type='graph') # # 無向グラフ

ノードの追加

1
2
node_a = pydot.Node("Node A")
node_b = pydot.Node("Node B")

エッジの追加

1
graph.add_edge(pydot.Edge(node_a, node_b))

グラフを画像として保存

1
graph.write_png('output.png')

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import itertools
import pydot
class Morph:
def __init__(self, surface, base, pos, pos1):
self.surface = surface
self.base = base
self.pos = pos
self.pos1 = pos1
def show(self):
print (self.surface, self.base, self.pos, self.pos1)

class Chunk:
def __init__(self, sentence_id, chunk_id, dst, srcs):
self.sentence_id = sentence_id
self.chunk_id = chunk_id
self.morphs = []
self.dst = dst
self.srcs = srcs
self.has_noun = False
self.has_verb = False
self.surfaces = ''
def show_morphs(self):
morphs = ''
for morph in self.morphs:
morphs += morph.surface
print ("morphs:",morphs)
def show_chunk_id(self):
print ("==========")
print ("chunk_id:",self.chunk_id)
def show_sentence_id(self):
if (self.chunk_id == 0):
print ("====================")
print ("sentence_id:",self.sentence_id)
def show_dst(self):
print ("dst:",self.dst)
def show_srcs(self):
print ("srcs:",self.srcs[self.chunk_id])

path = 'neko.txt.cabocha'
with open(path) as f:
text = f.read().split('\n')
result = []
morphs = []
chunks = []
srcs = [[]]
chunk = None
sentence_id = 0
chunk_id = 0

for line in text[:-1]:
if line == 'EOS':
result.append(morphs)
morphs = []
sentence_id += 1
chunk_id = 0
srcs = [[]]

elif line[0] == '*':
if chunk:
chunks.append(chunk)
dst = int(line.split()[2][:-1])
diff = dst + 1- len(srcs)
ex = [[] for _ in range(diff)]
srcs.extend(ex)
if dst!=-1:
srcs[dst].append(chunk_id)
chunk = Chunk(sentence_id, chunk_id, dst, srcs)
chunk_id += 1

else:
ls = line.split('\t')
d = {}
tmp = ls[1].split(',')
morph = Morph(ls[0],tmp[6],tmp[0],tmp[1])
morphs.append(morph)
chunk.morphs.append(morph)

else:
chunks.append(chunk)

sentences = [[] for _ in range(len(chunks))]
for chunk in chunks:
for morph in chunk.morphs:
chunk.surfaces += morph.surface

sentences[chunk.sentence_id].append(chunk)
graph = pydot.Dot(graph_type='digraph') # 無向グラフを指定したければ(graph_type='graph')とする
nodes = []
for chunk in sentences[7]: # 例として8番目の文章を使う
node = pydot.Node(chunk.surfaces)
graph.add_node(node)
nodes.append((node,chunk.dst))
for node, dst in nodes:
if dst != -1:
graph.add_edge(pydot.Edge(node, nodes[dst][0]))
graph.write_png('output44.png')

下記のような画像が出力されれば成功です。

45. 動詞の格パターンの抽出

今回用いている文章をコーパスと見なし,日本語の述語が取りうる格を調査したい. 動詞を述語,動詞に係っている文節の助詞を格と考え,述語と格をタブ区切り形式で出力せよ. ただし,出力は以下の仕様を満たすようにせよ.

  • 動詞を含む文節において,最左の動詞の基本形を述語とする
  • 述語に係る助詞を格とする
  • 述語に係る助詞(文節)が複数あるときは,すべての助詞をスペース区切りで辞書順に並べる

「吾輩はここで始めて人間というものを見た」という例文(neko.txt.cabochaの8文目)を考える. この文は「始める」と「見る」の2つの動詞を含み,「始める」に係る文節は「ここで」,「見る」に係る文節は「吾輩は」と「ものを」と解析された場合は,次のような出力になるはずである.

1
2
始める  で
見る は を

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

  • コーパス中で頻出する述語と格パターンの組み合わせ
  • 「する」「見る」「与える」という動詞の格パターン(コーパス中で出現頻度の高い順に並べよ)

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import itertools
class Morph:
def __init__(self, surface, base, pos, pos1):
self.surface = surface
self.base = base
self.pos = pos
self.pos1 = pos1
def show(self):
print (self.surface, self.base, self.pos, self.pos1)

class Chunk:
def __init__(self, sentence_id, chunk_id, dst, srcs):
self.sentence_id = sentence_id
self.chunk_id = chunk_id
self.morphs = []
self.dst = dst
self.srcs = srcs
self.has_noun = False
self.has_verb = False
self.has_particle = False
self.surfaces = ''
self.first_verb = None
self.particle = []
def show_morphs(self):
morphs = ''
for morph in self.morphs:
morphs += morph.surface
print ("morphs:",morphs)
def show_chunk_id(self):
print ("==========")
print ("chunk_id:",self.chunk_id)
def show_sentence_id(self):
if (self.chunk_id == 0):
print ("====================")
print ("sentence_id:",self.sentence_id)
def show_dst(self):
print ("dst:",self.dst)
def show_srcs(self):
print ("srcs:",self.srcs[self.chunk_id])

path = 'neko.txt.cabocha'
with open(path) as f:
text = f.read().split('\n')
result = []
morphs = []
chunks = []
srcs = [[]]
chunk = None
sentence_id = 0
chunk_id = 0

for line in text[:-1]:
if line == 'EOS':
result.append(morphs)
morphs = []
sentence_id += 1
chunk_id = 0
srcs = [[]]

elif line[0] == '*':
if chunk:
chunks.append(chunk)
dst = int(line.split()[2][:-1])
diff = dst + 1- len(srcs)
ex = [[] for _ in range(diff)]
srcs.extend(ex)
if dst!=-1:
srcs[dst].append(chunk_id)
chunk = Chunk(sentence_id, chunk_id, dst, srcs)
chunk_id += 1

else:
ls = line.split('\t')
d = {}
tmp = ls[1].split(',')
morph = Morph(ls[0],tmp[6],tmp[0],tmp[1])
morphs.append(morph)
chunk.morphs.append(morph)

else:
chunks.append(chunk)

sentences = [[] for _ in range(len(chunks))]
for chunk in chunks:
for morph in chunk.morphs:
chunk.surfaces += morph.surface
if morph.pos == '動詞':
if chunk.has_verb == False:
chunk.first_verb = morph.base
chunk.has_verb = True
elif morph.pos == '名詞':
chunk.has_noun = True
elif morph.pos == '助詞':
chunk.has_particle = True
chunk.particle.append(morph.surface)


sentences[chunk.sentence_id].append(chunk)

dsts = [[] for _ in range(len(chunks))]
for chunk in chunks:
dst = sentences[chunk.sentence_id][chunk.dst]
dsts[chunk.sentence_id].append(dst)

with open('45.txt', mode='w') as f:
for i,(sentence,dst) in enumerate(zip(sentences,dsts)):
dic = {}
for s,d in zip(sentence,dst):
if s.particle and d.first_verb:
old = dic.get(d.first_verb, [])
dic[d.first_verb] = old + s.particle

for k,v in dic.items():
output = k+'\t'+" ".join(sorted(v))+'\n'
f.write(output)

確認

1
sort 45.txt | uniq -c | sort -n -r | head
1
2
3
4
5
6
7
8
9
10
417 云う	と
256 する を
212 思う と
158 ある が
156 見る て
145 なる に
110 する に
87 見える と
81 見る を
68 する と
1
grep "^する\t" 45.txt | sort | uniq -c | sort -n -r | head
1
2
3
4
5
6
7
8
9
10
256 する	を
110 する に
68 する と
67 する に を
67 する が
54 する て を
37 する は
36 する が を
30 する で を
30 する て
1
grep "^見る\t" 45.txt | sort | uniq -c | sort -n -r | head
1
2
3
4
5
6
7
8
9
10
156 見る	て
81 見る を
17 見る から
16 見る て て
14 見る て を
12 見る から て
12 見る と
10 見る で
9 見る て は
7 見る に
1
grep "^与える\t" 45.txt | sort | uniq -c | sort -n -r | head
1
2
3
4
5
6
7
8
9
10
3 与える	に を
2 与える て に は を
1 与える けれども に は を
1 与える から が て て と に は は を
1 与える つつ て に に は を
1 与える だけ で に を
1 与える たり て に を
1 与える に に対して のみ は は も
1 与える か じゃあ て と は を
1 与える か として ば を を

46. 動詞の格フレーム情報の抽出

45のプログラムを改変し,述語と格パターンに続けて項(述語に係っている文節そのもの)をタブ区切り形式で出力せよ.45の仕様に加えて,以下の仕様を満たすようにせよ.

  • 項は述語に係っている文節の単語列とする(末尾の助詞を取り除く必要はない)
  • 述語に係る文節が複数あるときは,助詞と同一の基準・順序でスペース区切りで並べる

「吾輩はここで始めて人間というものを見た」という例文(neko.txt.cabochaの8文目)を考える. この文は「始める」と「見る」の2つの動詞を含み,「始める」に係る文節は「ここで」,「見る」に係る文節は「吾輩は」と「ものを」と解析された場合は,次のような出力になるはずである.

1
2
始める  で      ここで
見る は を 吾輩は ものを

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import itertools
class Morph:
def __init__(self, surface, base, pos, pos1):
self.surface = surface
self.base = base
self.pos = pos
self.pos1 = pos1
def show(self):
print (self.surface, self.base, self.pos, self.pos1)

class Chunk:
def __init__(self, sentence_id, chunk_id, dst, srcs):
self.sentence_id = sentence_id
self.chunk_id = chunk_id
self.morphs = []
self.dst = dst
self.srcs = srcs
self.has_noun = False
self.has_verb = False
self.has_particle = False
self.surfaces = ''
self.first_verb = None
self.particle = []
def show_morphs(self):
morphs = ''
for morph in self.morphs:
morphs += morph.surface
print ("morphs:",morphs)
def show_chunk_id(self):
print ("==========")
print ("chunk_id:",self.chunk_id)
def show_sentence_id(self):
if (self.chunk_id == 0):
print ("====================")
print ("sentence_id:",self.sentence_id)
def show_dst(self):
print ("dst:",self.dst)
def show_srcs(self):
print ("srcs:",self.srcs[self.chunk_id])

path = 'neko.txt.cabocha'
with open(path) as f:
text = f.read().split('\n')
result = []
morphs = []
chunks = []
srcs = [[]]
chunk = None
sentence_id = 0
chunk_id = 0

for line in text[:-1]:
if line == 'EOS':
result.append(morphs)
morphs = []
sentence_id += 1
chunk_id = 0
srcs = [[]]

elif line[0] == '*':
if chunk:
chunks.append(chunk)
dst = int(line.split()[2][:-1])
diff = dst + 1- len(srcs)
ex = [[] for _ in range(diff)]
srcs.extend(ex)
if dst!=-1:
srcs[dst].append(chunk_id)
chunk = Chunk(sentence_id, chunk_id, dst, srcs)
chunk_id += 1

else:
ls = line.split('\t')
d = {}
tmp = ls[1].split(',')
morph = Morph(ls[0],tmp[6],tmp[0],tmp[1])
morphs.append(morph)
chunk.morphs.append(morph)

else:
chunks.append(chunk)

sentences = [[] for _ in range(len(chunks))]
for chunk in chunks:
for morph in chunk.morphs:
chunk.surfaces += morph.surface
if morph.pos == '動詞':
if chunk.has_verb == False:
chunk.first_verb = morph.base
chunk.has_verb = True
elif morph.pos == '名詞':
chunk.has_noun = True
elif morph.pos == '助詞':
chunk.has_particle = True
chunk.particle.append(morph.surface)


sentences[chunk.sentence_id].append(chunk)

dsts = [[] for _ in range(len(chunks))]
for chunk in chunks:
dst = sentences[chunk.sentence_id][chunk.dst]
dsts[chunk.sentence_id].append(dst)

with open('46.txt', mode='w') as f:
for i,(sentence,dst) in enumerate(zip(sentences,dsts)):
dic = {}

for s,d in zip(sentence,dst):
if s.particle and d.first_verb:
old = dic.get(d.first_verb, [])
surfaces = s.surfaces.replace(" ","").replace("。","").replace("、","")
for p in s.particle:
dic[d.first_verb] = old + [[p, surfaces]]
for k,v in dic.items():
ls = sorted(v)
ls = list(zip(*ls))
output = k+'\t'+" ".join(ls[0])+'\t'+" ".join(ls[1])+'\n'
f.write(output)

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

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

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

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

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

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

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

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import itertools
class Morph:
def __init__(self, surface, base, pos, pos1):
self.surface = surface
self.base = base
self.pos = pos
self.pos1 = pos1

def show(self):
print (self.surface, self.base, self.pos, self.pos1)

class Chunk:
def __init__(self, sentence_id, chunk_id, dst, srcs):
self.sentence_id = sentence_id
self.chunk_id = chunk_id
self.morphs = []
self.dst = dst
self.srcs = srcs
self.has_noun = False
self.has_verb = False
self.has_particle = False
self.surfaces = ''
self.first_verb = None
self.particle = []
self.sahen_wo = False

def show_morphs(self):
morphs = ''
for morph in self.morphs:
morphs += morph.surface
print ("morphs:",morphs)
def show_chunk_id(self):
print ("==========")
print ("chunk_id:",self.chunk_id)
def show_sentence_id(self):
if (self.chunk_id == 0):
print ("====================")
print ("sentence_id:",self.sentence_id)
def show_dst(self):
print ("dst:",self.dst)
def show_srcs(self):
print ("srcs:",self.srcs[self.chunk_id])

path = 'neko.txt.cabocha'

with open(path) as f:
text = f.read().split('\n')
result = []
morphs = []
chunks = []
srcs = [[]]
chunk = None
sentence_id = 0
chunk_id = 0

for line in text[:-1]:
if line == 'EOS':
result.append(morphs)
morphs = []
sentence_id += 1
chunk_id = 0
srcs = [[]]

elif line[0] == '*':
if chunk:
chunks.append(chunk)
dst = int(line.split()[2][:-1])
diff = dst + 1- len(srcs)
ex = [[] for _ in range(diff)]
srcs.extend(ex)
if dst!=-1:
srcs[dst].append(chunk_id)
chunk = Chunk(sentence_id, chunk_id, dst, srcs)
chunk_id += 1

else:
ls = line.split('\t')
d = {}
tmp = ls[1].split(',')
morph = Morph(ls[0],tmp[6],tmp[0],tmp[1])
morphs.append(morph)
chunk.morphs.append(morph)

else:
chunks.append(chunk)

sentences = [[] for _ in range(len(chunks))]
sahen_wo = None
for chunk in chunks:
sahen = False

for morph in chunk.morphs:

chunk.surfaces += morph.surface

if morph.pos == '動詞':

if chunk.has_verb == False and sahen_wo:
chunk.first_verb = sahen_wo + morph.base
sahen_wo = None
chunk.has_verb = True

elif morph.surface == 'を' and morph.pos == '助詞' and sahen:
sahen_wo = sahen + 'を'


elif morph.pos == '助詞':
chunk.has_particle = True
chunk.particle.append(morph.surface)

if morph.pos1 == 'サ変接続':
sahen = morph.surface
else:
sahen = False

sentences[chunk.sentence_id].append(chunk)

dsts = [[] for _ in range(len(chunks))]
for chunk in chunks:
dst = sentences[chunk.sentence_id][chunk.dst]
dsts[chunk.sentence_id].append(dst)

with open('47.txt', mode='w') as f:
for i,(sentence,dst) in enumerate(zip(sentences,dsts)):
dic = {}

for s,d in zip(sentence,dst):
if s.particle and d.first_verb:
old = dic.get(d.first_verb, [])
surfaces = s.surfaces.replace(" ","").replace("。","").replace("、","")
for p in s.particle:
dic[d.first_verb] = old + [[p, surfaces]]
for k,v in dic.items():
ls = sorted(v)
ls = list(zip(*ls))
output = k+'\t'+" ".join(ls[0])+'\t'+" ".join(ls[1])+'\n'
print (output)
f.write(output)

文節をまたいで、サ変接続名詞+を(助詞)扱うためにグローバル変数sahen_woを用意しましたが、もう少し良い方法がありそうです。

1
cut -f 1  47.txt | sort | uniq -c | sort -n -r | head
1
2
3
4
5
6
7
8
9
10
26 返事をする
19 挨拶をする
12 話をする
8 質問をする
7 喧嘩をする
6 真似をする
5 質問をかける
5 相談をする
5 注意をする
5 昼寝をする
1
cut -f 1,2  47.txt | sort | uniq -c | sort -n -r | head
1
2
3
4
5
6
7
8
9
10
6 返事をする	と
4 返事をする と は
4 挨拶をする と
3 質問をかける と は
3 挨拶をする から
3 喧嘩をする と
2 同情を表する て と は
2 休養を要する は
2 挨拶をする と も
2 講義をする で

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

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

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

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

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

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import itertools
class Morph:
def __init__(self, surface, base, pos, pos1):
self.surface = surface
self.base = base
self.pos = pos
self.pos1 = pos1

def show(self):
print (self.surface, self.base, self.pos, self.pos1)

class Chunk:
def __init__(self, sentence_id, chunk_id, dst, srcs):
self.sentence_id = sentence_id
self.chunk_id = chunk_id
self.morphs = []
self.dst = dst
self.srcs = srcs
self.has_noun = False
self.has_verb = False
self.has_particle = False
self.surfaces = ''
self.first_verb = None
self.particle = []
self.sahen_wo = False

def show_morphs(self):
morphs = ''
for morph in self.morphs:
morphs += morph.surface
print ("morphs:",morphs)
def show_chunk_id(self):
print ("==========")
print ("chunk_id:",self.chunk_id)
def show_sentence_id(self):
if (self.chunk_id == 0):
print ("====================")
print ("sentence_id:",self.sentence_id)
def show_dst(self):
print ("dst:",self.dst)
def show_srcs(self):
print ("srcs:",self.srcs[self.chunk_id])

path = 'neko.txt.cabocha'
with open(path) as f:
text = f.read().split('\n')
result = []
morphs = []
chunks = []
srcs = [[]]
chunk = None
sentence_id = 0
chunk_id = 0

for line in text[:-1]:
if line == 'EOS':
result.append(morphs)
morphs = []
sentence_id += 1
chunk_id = 0
srcs = [[]]

elif line[0] == '*':
if chunk:
chunks.append(chunk)
dst = int(line.split()[2][:-1])
diff = dst + 1- len(srcs)
ex = [[] for _ in range(diff)]
srcs.extend(ex)
if dst!=-1:
srcs[dst].append(chunk_id)
chunk = Chunk(sentence_id, chunk_id, dst, srcs)
chunk_id += 1

else:
ls = line.split('\t')
d = {}
tmp = ls[1].split(',')
morph = Morph(ls[0],tmp[6],tmp[0],tmp[1])
morphs.append(morph)
chunk.morphs.append(morph)

else:
chunks.append(chunk)

sentences = [[] for _ in range(len(chunks))]
for chunk in chunks:
morphs = ''
for morph in chunk.morphs:
morphs += morph.surface
if morph.pos == '名詞':
chunk.has_noun = True
sentences[chunk.sentence_id].append([morphs,chunk.dst,chunk.has_noun])

def rec(sentence,d,ans):
if d == -1:
return ans
else:
return rec(sentence,sentence[d][1],ans+' -> '+sentence[d][0])

with open('48.txt', mode='w') as f:
for i, sentence in enumerate(sentences):
for s,d,has_noun in sentence:
if has_noun:
ans = rec(sentence,d,s)
ans = ans.replace(" ","").replace("。","").replace("、","")
print (ans)
f.write(ans+'\n')

再帰関数recを導入した。

49. 名詞間の係り受けパスの抽出

文中のすべての名詞句のペアを結ぶ最短係り受けパスを抽出せよ.ただし,名詞句ペアの文節番号がiとj(i<j)のとき,係り受けパスは以下の仕様を満たすものとする

  • 問題48と同様に,パスは開始文節から終了文節に至るまでの各文節の表現(表層形の形態素列)を” -> “で連結して表現する
  • 文節iとjに含まれる名詞句はそれぞれ,XとYに置換する

また,係り受けパスの形状は,以下の2通りが考えられる.

  • 文節iから構文木の根に至る経路上に文節jが存在する場合: 文節iから文節jのパスを表示
  • 上記以外で,文節iと文節jから構文木の根に至る経路上で共通の文節kで交わる場合: 文節iから文節kに至る直前のパスと文節jから文節kに至る直前までのパス,文節kの内容を” | “で連結して表示

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

1
2
3
4
5
6
Xは | Yで -> 始めて -> 人間という -> ものを | 見た
Xは | Yという -> ものを | 見た
Xは | Yを | 見た
Xで -> 始めて -> Y
Xで -> 始めて -> 人間という -> Y
Xという -> Y
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import itertools
class Morph:
def __init__(self, surface, base, pos, pos1):
self.surface = surface
self.base = base
self.pos = pos
self.pos1 = pos1

def show(self):
print (self.surface, self.base, self.pos, self.pos1)

class Chunk:
def __init__(self, sentence_id, chunk_id, dst, srcs):
self.sentence_id = sentence_id
self.chunk_id = chunk_id
self.morphs = []
self.dst = dst
self.srcs = srcs
self.has_noun = False
self.has_verb = False
self.has_particle = False
self.surfaces = ''
self.first_verb = None
self.first_noun = None
self.particle = []
self.sahen_wo = False

def show_morphs(self):
morphs = ''
for morph in self.morphs:
morphs += morph.surface
print ("morphs:",morphs)
def show_chunk_id(self):
print ("==========")
print ("chunk_id:",self.chunk_id)
def show_sentence_id(self):
if (self.chunk_id == 0):
print ("====================")
print ("sentence_id:",self.sentence_id)
def show_dst(self):
print ("dst:",self.dst)
def show_srcs(self):
print ("srcs:",self.srcs[self.chunk_id])

path = 'neko.txt.cabocha'
with open(path) as f:
text = f.read().split('\n')
result = []
morphs = []
chunks = []
srcs = [[]]
chunk = None
sentence_id = 0
chunk_id = 0

for line in text[:-1]:
if line == 'EOS':
result.append(morphs)
morphs = []
sentence_id += 1
chunk_id = 0
srcs = [[]]

elif line[0] == '*':
if chunk:
chunks.append(chunk)
dst = int(line.split()[2][:-1])
diff = dst + 1- len(srcs)
ex = [[] for _ in range(diff)]
srcs.extend(ex)
if dst!=-1:
srcs[dst].append(chunk_id)
chunk = Chunk(sentence_id, chunk_id, dst, srcs)
chunk_id += 1

else:
ls = line.split('\t')
d = {}
tmp = ls[1].split(',')
morph = Morph(ls[0],tmp[6],tmp[0],tmp[1])
morphs.append(morph)
chunk.morphs.append(morph)

else:
chunks.append(chunk)

sentences = [[] for _ in range(len(chunks))]
for chunk in chunks:
morphs = ''
for morph in chunk.morphs:

if morph.pos == '名詞':
chunk.has_noun = True
chunk.first_noun = morph.surface
morphs += morph.surface
sentences[chunk.sentence_id].append([morphs,chunk.dst,chunk.has_noun,chunk.first_noun])

def rec(sentence,d,ans):
if d == -1:
return ans
else:
return rec(sentence,sentence[d][1],ans+' -> '+sentence[d][0])
def get_path(d,ls):
ls += [d]
if d == -1:
return ls
else:
return get_path(sentence[d][1],ls)

def gen_arrow(path, xy):
new = []
for i,node in enumerate(path):
if node == match:
break
morph = sentence[node][0]
if i==0:
morph = morph.replace(sentence[node][3], xy)
new.append(morph)
return ' -> '.join(new)

with open('49.txt', mode='w') as f:
for i, sentence in enumerate(sentences):
# if i!=7:
# continue
ls_path = []
for j,(s,d,has_noun,first_noun) in enumerate(sentence):
if has_noun:
ans = rec(sentence,d,s)
ans = ans.replace(" ","").replace("。","").replace("、","")
ls_path.append(get_path(d,[j]))

for pi, pj in itertools.combinations(ls_path,2):
for ni in pi:
if ni in pj:
match = ni
break
ans = ''
ans += gen_arrow(pi, 'X')
ans += ' | '
ans += gen_arrow(pj, 'Y')
ans += ' | '

if '| |' in ans:
ans = ans.replace('| |','->')
ans += 'Y'
else:
ans += sentence[match][0]
ans = ans.replace(" ","").replace("。","").replace("、","")

print (ans)
f.write(ans+'\n')

最後に

全100問の解説に戻る

記事情報

  • 投稿日:2020年5月9日
  • 最終更新日:2020年5月9日