TPTブログ

テックポート株式会社のブログです。 技術情報や製品・サービス情報、 また未経験社員がデータサイエンティストを 目指す奮闘記など、更新していきます。

▲心くじけず言語処理100本ノック==40~44==

f:id:TBT_matsu:20200515145048p:plain
こんにちは!
テービーテックの村松です。

「言語処理100本ノック2020]」
nlp100.github.io
に挑戦中!
途中でくじけないか見守ってください・・・。
そして、皆さんも一緒に挑戦してみましょう!

本日は第5章: 係り受け解析 40~44です!
間違い・コード改善点などありましたら教えていただけると嬉しいです。

第5章: 係り受け解析

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

下準備はこちら↓↓で行いました。
ds-blog.tbtech.co.jp

データの読み込み
#テキストデータの読み込み
path = 'neko.txt.cabocha'
import re
with open(path, encoding='utf-8') as f:
  _data = f.read().split('\n')

_dataの中身は3種類に分かれます。

①係り受け解析の行
「*」で始まります。
内容はスペース区切りで、
「* 文節番号 係り先の文節番号(係り先がない時は-1) 主辞の形態素番号/機能語の形態素番号 係り関係のスコア(大きい方が係りやすい)」

#例:係り受け解析の行
_data[6]
##結果
'* 1 2D 0/1 -0.764522'

②形態素解析結果の行
タブと「,」区切りで、
表層形\t品詞,品詞細分類1.品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音
の順番に入っています。

#例:形態素解析結果の行
_data[7]
##結果
'吾輩\t名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ'

③文の終わりを示す「EOS」

#例:EOS
_data[2]
##結果
'EOS'
40.係り受け解析結果の読み込み(形態素)

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

クラスMorph
class Morph:
  def __init__(self, word):
    self.surface = word[0]
    self.base = word[7]
    self.pos = word[1]
    self.pos1 = word[2]
#一文ずつにまとめたリスト
sent = []
#sentに入れるリストの仮置き場
temp = []

for line in _data[:-1]:
  #リスト内の各要素を分割します。
  #集合[]で「\t」と「,」と「 (スペース)」を指定します。
  text = re.split("[\t, ]", line)
  #「EOS」を目印に1文ごとにリストにまとめます。
  if text[0] == 'EOS':
    sent.append(temp)
    #次の文に使用するために空にします。
    temp = []
  #係り受け解析の行は今回は不要なのでcontinue
  elif text[0] == '*':
    continue
  #形態素解析の結果から指定の要素をMorphオブジェクトのリストとしてtempに格納します。
  else:
    morph = Morph(text)
    temp.append(morph)

#3文目の表示をします。
for morph in sent[2]:
    print(vars(morph))
##結果
{'surface': '\u3000', 'base': '\u3000', 'pos': '記号', 'pos1': '空白'}
{'surface': '吾輩', 'base': '吾輩', 'pos': '名詞', 'pos1': '代名詞'}
{'surface': 'は', 'base': 'は', 'pos': '助詞', 'pos1': '係助詞'}
{'surface': '猫', 'base': '猫', 'pos': '名詞', 'pos1': '一般'}
{'surface': 'で', 'base': 'だ', 'pos': '助動詞', 'pos1': '*'}
{'surface': 'ある', 'base': 'ある', 'pos': '助動詞', 'pos1': '*'}
{'surface': '。', 'base': '。', 'pos': '記号', 'pos1': '句点'}
41.係り受け解析結果の読み込み(文節・係り受け)

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

#40で作成したクラスMorph
class Morph:
  def __init__(self, word):
    self.surface = word[0]
    self.base = word[7]
    self.pos = word[1]
    self.pos1 = word[2]
#クラスChunk
class Chunk:
  def __init__(self, idx, dst):
    self.idx = idx     #文節番号
    self.morphs = []   #形態素(Morphオブジェクト)のリスト
    self.dst = dst     #係り先文節インデックス番号
    self.srcs = []     #係り元文節インデックス番号のリスト

import re
#1文ごとのリスト
s_list = []
#Chunkオブジェクト
sent = []
#形態素解析結果のMorphオブジェクトリスト
temp = []
chunk = None
for line in _data[:-1]:
  #集合[]で「\t」と「,」と「 (スペース)」を区切りを指定します。
  text = re.split("[\t, ]", line)

  #係り受け解析の行の処理
  if text[0] == '*':
    idx = int(text[1])
    dst = int(re.search(r'(.*?)D', text[2]).group(1))
    #Chunkオブジェクトへ
    chunk = Chunk(idx, dst)
    sent.append(chunk)

  #EOSを目印に文ごとにリスト化
  elif text[0] == 'EOS':
    if sent:
      for i, c in enumerate(sent, 0):
        if c.dst == -1:
          continue
        else:
          sent[c.dst].srcs.append(i)
      s_list.append(sent)
    sent = []
  else:
    morph = Morph(text)
    chunk.morphs.append(morph)
    temp.append(morph)

#8行目の表示
for m in s_list[7]:
  print(m.idx, [mo.surface for mo in m.morphs], '係り元:' + str(m.srcs),'係り先:' + str(m.dst))
##結果
0 ['この'] 係り元:[] 係り先:1
1 ['書生', 'という', 'の', 'は'] 係り元:[0] 係り先:7
2 ['時々'] 係り元:[] 係り先:4
3 ['我々', 'を'] 係り元:[] 係り先:4
4 ['捕え', 'て'] 係り元:[2, 3] 係り先:5
5 ['煮', 'て'] 係り元:[4] 係り先:6
6 ['食う', 'という'] 係り元:[5] 係り先:7
7 ['話', 'で', 'ある', '。'] 係り元:[1, 6] 係り先:-1

長くなるので逐一書きませんが、
以降の問題はこの問題の処理を行ったことを前提としていきます。
ご了承を。

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

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

for s in s_list:
  for m in s:
    #係り先がある文節の場合、
    if int(m.dst) != -1:
      #形態素解析結果のposが'記号'以外のものをrタブ区切りで表示します。
      print(''.join([b.surface if b.pos != '記号' else '' for b in m.morphs]),
            ''.join([b.surface if b.pos != '記号' else '' for b in s[int(m.dst)].morphs]), sep='\t')

##結果(長くなるので8文目を抜粋して表示します)
この	書生というのは
書生というのは	話である
時々	捕えて
我々を	捕えて
捕えて	煮て
煮て	食うという
食うという	話である
43.名詞を含む文節が動詞を含む文節に係るものを抽出

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

for s in s_list:
  for m in s:
    #係り先がある文節の場合、
    if int(m.dst) != -1:
   #記号以外の表層形を抽出
      a = ''.join([b.surface if b.pos != '記号' else '' for b in m.morphs])
      b = ''.join([b.surface if b.pos != '記号' else '' for b in s[int(m.dst)].morphs])      
      a_p = [b.pos for b in m.morphs]
      b_p = [b.pos for b in s[int(m.dst)].morphs]
   #係り元に名詞が含まれて、かつ、係り先に動詞があるものを表示します
      if '名詞' in a_p and '動詞' in b_p:
        print(a, b, sep='\t')

##結果(長くなるので8文目を抜粋して表示します)
我々を	捕えて
44.係り受け木の可視化

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

#8行目を試しにやってみます
v = s_list[7]

#文節のセットを格納するリストの作成
s_pairs = []
for m in v:
  if int(m.dst) != -1:      
 #係り先と係り元の文節のタプルを作成し、リストに格納していきます。
    a = ''.join([b.surface if b.pos != '記号' else '' for b in m.morphs])
    b = ''.join([b.surface if b.pos != '記号' else '' for b in v[int(m.dst)].morphs])
    c = a, b
    s_pairs.append(c)

#係り受け木の描画
import pydot_ng as pydot
img = pydot.Dot(graph_type='digraph')
#日本語に対応しているフォントを指定します
img.set_node_defaults(fontname='Meiryo UI', fontsize='10')
for s, t in s_pairs:
  img.add_edge(pydot.Edge(s, t))
img.write_png('画像保存先/44')

f:id:TBT_matsu:20200612124320p:plain


ここまでご覧いただきありがとうございます。
以上、第5章40~44でした!