【言語処理100本ノック解いてみた】 第5章:係り受け解析 No.45~No.49

プログラミング

はじめに

今回は第5章の後半を解いていきたいと思います.
第5章の後半では,文章を係り受け解析し,文章ないの特定のパターンを抽出することや係り受け結果を可視化することを実施しています.

第5章の前半で用いた機能や結果を使うことになるので以前投稿した内容を参照しながらご確認ください.
また,第5章の前半で作成した”ai.ja.txt.parsed”を利用する点にご留意ください.

 

第5章前半の再利用コード

第5章の前半で利用した下記のコードを第5章の後半でも用いていきますので,参考にしてください.
#Morphオブジェクトの作成
class Morph:
    def __init__(self, morph):
        surface,attr = morph.split("\t")
        attr = attr.split(",")
        self.surface = surface
        self.base = attr[6]
        self.pos = attr[0]
        self.pos1 = attr[1]

#Chunkオブジェクトの作成
class Chunk:
    def __init__(self, morphs, dst):
        self.morphs = morphs
        self.dst = dst
        self.srcs = []

sentence_chunk = []
chunk_list = []
morphs = []
for s in blocks:
    block = s.split("\n")
    if block[0] == "" or len(block) ==0:
        continue
    for b in block:
        if b =="":
            continue
        #blockの先頭が*の時,係り受けの内容が記述
        elif b[0] == "*":
            if len(morphs) > 0:
                chunk_list.append(Chunk(morphs,dst))
            #chunk_id, 係り先の番号, 主辞(文節の中心となる単語)/機能語(助詞など)の位置, Score
            dep = b.split(" ")
            dst = dep[2][:-1]
            morphs = []
            continue
        morphs.append(Morph(b))
    chunk_list.append(Chunk(morphs, dst))

    #一文分のChunkに対して係り元の文節を追加
    for i, c in enumerate(chunk_list):
        if c.dst != "-1":
            chunk_list[int(c.dst)].srcs.append(i)
    sentence_chunk.append(chunk_list)
    chunk_list = []
    morphs = []
for c in sentence_chunk[1]:
    print([m.surface for m in c.morphs], c.dst, c.srcs)

上記コードで作成した,ChunkオブジェクトやMorphオブジェクト,また,それを格納しているsentence_chunkなどをこの後の問題で利用しています.

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

問題文:
今回用いている文章をコーパスと見なし,日本語の述語が取りうる格を調査したい.
動詞を述語,動詞に係っている文節の助詞を格と考え,述語と格をタブ区切り形式で出力せよ.
ただし,出力は以下の仕様を満たすようにせよ.・動詞を含む文節において,最左の動詞の基本形を述語とする
・述語に係る助詞を格とする
・述語に係る助詞(文節)が複数あるときは,すべての助詞をスペース区切りで辞書順に並べるこのプログラムの出力をファイルに保存し,以下の事項をUNIXコマンドを用いて確認せよ.
コーパス中で頻出する述語と格パターンの組み合わせ 「行う」「なる」「与える」という動詞の格パターン(コーパス中で出現頻度の高い順に並べよ)
コマンド
output_list = []
for i in range(len(sentence_chunk)):
    for c in sentence_chunk[i]:
        for m in c.morphs:
            # print(m.surface)
            if m.pos =="動詞":
                particle_list = []
                for src in c.srcs:
                    particle = [m.surface for m in sentence_chunk[i][src].morphs if m.pos == "助詞"]
                    if particle:
                        particle_list.append(particle[-1])
                particle_list = list(set(particle_list))
                particle_list.sort()
                output = m.base+"\t"+" ".join(particle_list)
                output_list.append(output)
                break
for i in range(10):
    print(output_list[i])

with open("predicate_particle.txt",mode="w") as f:
     f.write('\n'.join(output_list))

出力

!sort predicate_particle.txt | uniq -c | sort -nr |grep -e "行う" -e "なる" -e "与える"
#出力
      9 行う	を
      7 行う	て に
      5 なる	から で と
      3 行う	て に を
      2 与える	が に
      1 行う	まで を
      1 行う	から
      :
解説

動詞の抽出とそれに係る助詞を抽出する問題です.動詞に関しては最左のものをとのことなので,文節を順に見たときに最初に出てきた動詞を補足すれば良いです.その動詞に対して,係り元を探し,係り元の文節の助詞のみを抽出します.この時,助詞の中でも最後に追加された助詞のみを抽出しています.集めた助詞の要素を最後にset()で重複をなくし,sortすれば辞書順に並んだ助詞のリストができます.最後に作成した動詞と助詞のリストをファイルに出力すれば問題を解くことができます.
また,出力の確認は,sortで順番に並べ,uniqで同じ出力をまとめ,sortで重複数順に並び替え,grepで対象の”行う”,”なる”,”与える”のみを出力するようにして確認しています.

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

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

  • 項は述語に係っている文節の単語列とする(末尾の助詞を取り除く必要はない)
  • 述語に係る文節が複数あるときは,助詞と同一の基準・順序でスペース区切りで並べる
コマンド
output_list = []
output_list = []
for i in range(len(sentence_chunk)):
    for c in sentence_chunk[i]:
        for m in c.morphs:
            # print(m.surface)
            if m.pos =="動詞":
                particle_list = []
                clause_list = []
                for src in c.srcs:
                    particle = [m.surface for m in sentence_chunk[i][src].morphs if m.pos == "助詞"]
                    if particle:
                        particle_list.append(particle[-1])
                        clause = "".join([m.surface for m in sentence_chunk[i][src].morphs])
                        clause_list.append((particle, clause))
                    
                particle_list.sort()
                clause_list.sort()
                clause_output = " ".join([clause[1] for clause in clause_list])
                output = m.base+"\t"+" ".join(particle_list) +" "+ clause_output
                output_list.append(output)
                break

for i in range(10):
    print(output_list[i])

出力

用いる	用いる	を 道具を
する	て を 用いて 『知能』を
指す	を 一分野」を
代わる	に を 人間に 知的行動を
行う	て に 代わって コンピューターに
する	も 研究分野」とも
述べる	で に は 解説で、 次のように 佐藤理史は
する	で を コンピュータ上で 知的能力を
する	を 推論・判断を
する	を 画像データを
解説

45で解いたコードを少し改変するだけで解くことができます.助詞が文節内に含まれていた場合,その文節の単語の表層系を繋げて,一つの文節としてリストの要素に保持します.

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

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

  • 「サ変接続名詞+を(助詞)」で構成される文節が動詞に係る場合のみを対象とする
  • 述語は「サ変接続名詞+を+動詞の基本形」とし,文節中に複数の動詞があるときは,最左の動詞を用いる
  • 述語に係る助詞(文節)が複数あるときは,すべての助詞をスペース区切りで辞書順に並べる
  • 述語に係る文節が複数ある場合は,すべての項をスペース区切りで並べる(助詞の並び順と揃えよ)
コマンド
output_list = []
for i in range(len(sentence_chunk)):
    for c in sentence_chunk[i]:
        for m in c.morphs:
            if m.pos == "動詞":
                particle_list = []
                clause_list = []
                mining_result = ""
                for src in c.srcs:
                    if len(sentence_chunk[i][src].morphs) == 2 and\
                      sentence_chunk[i][src].morphs[0].pos == "名詞" and \
                      sentence_chunk[i][src].morphs[0].pos1 == "サ変接続" and \
                      sentence_chunk[i][src].morphs[1].surface == "を":
                      
                      mining_result = sentence_chunk[i][src].morphs[0].surface+"を"+m.base
                    else:
                        particle = [m.surface for m in sentence_chunk[i][src].morphs if m.pos == "助詞"]
                        if particle:
                            particle_list.append(particle[-1])
                            clause = "".join([m.surface for m in sentence_chunk[i][src].morphs])
                            clause_list.append((particle[-1], clause))
                if len(mining_result) != 0 and len(particle_list) > 0:
                    particle_list.sort()
                    clause_list.sort()
                    clause_output = " ".join([clause[1] for clause in clause_list])
                    output = mining_result+"\t"+" ".join(particle_list) +" "+ clause_output
                    print(output)
                    output_list.append(output)                            
                break

出力

用いる	記述をする	と 主体と
注目を集める	が 「サポートベクターマシン」が
学習を行う	に 元に
進化を見せる	て て において は 加えて、 活躍している。 生成技術において (敵対的生成ネットワーク)は、
開発を行う	は エイダ・ラブレスは
意味をする	に データに
研究を進める	て 費やして
命令をする	で 機構で
        :
解説

46で解いたコードに条件を追記することで問題を解くことができます.最初に出てきた動詞の係り元の中に,サ変接続名詞と「を」で構成された文節があれば,抽出します.サ変接続名詞と「を」抽出が確認できた場合,かつ,係り元に助詞が他にもある場合に結果の出力を作成しています.

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

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

  • 各文節は(表層形の)形態素列で表現する
  • パスの開始文節から終了文節に至るまで,各文節の表現を” -> “で連結する
コマンド
output_list = []
for i in range(len(sentence_chunk)):
    for c in sentence_chunk[i]:
        pos_list = [m.pos for m in c.morphs]
        if "名詞" in pos_list:
            surface_list = [m.surface for m in c.morphs if m.pos != '記号']
            path_list = ["".join(surface_list)]
            dst = int(c.dst)
            while dst != -1:
                surface_list = [m.surface for m in sentence_chunk[i][dst].morphs if m.pos != '記号']
                path_list.append("".join(surface_list))
                dst = int(sentence_chunk[i][dst].dst)
            if len(path_list) > 1:
                print("->".join(path_list))

出力

人工知能->語->研究分野とも->される
じんこうちのう->語->研究分野とも->される
AI->エーアイとは->語->研究分野とも->される
エーアイとは->語->研究分野とも->される
計算->という->道具を->用いて->研究する->計算機科学->の->一分野を->指す->語->研究分野とも->される
概念と->道具を->用いて->研究する->計算機科学->の->一分野を->指す->語->研究分野とも->される
コンピュータ->という->道具を->用いて->研究する->計算機科学->の->一分野を->指す->語->研究分野とも->される
道具を->用いて->研究する->計算機科学->の->一分野を->指す->語->研究分野とも->される
知能を->研究する->計算機科学->の->一分野を->指す->語->研究分野とも->される
                                :
解説

段々と構文解析が進んでいきましたね.まずは,名詞を含む文節の出現を検知します.その後,その文節の係り先を再帰的に探し,根までに通る文節を取得します.最後にそれらの文節を”->”で繋ぐことで出力を作成することができます.

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

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

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

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

  • 文節から構文木の根に至る経路上に文節
    j
     

    が存在する場合: 文節から文節のパスを表示

  • 上記以外で,文節と文節から構文木の根に至る経路上で共通の文節で交わる場合: 文節から文節に至る直前のパスと文節から文節に至る直前までのパス,文節の内容を” | “で連結して表示
コマンド
def make_path(path_list, phrase_type):
    path = []
    for p in path_list:
        phrase = "".join([sur_pos[0] for sur_pos in p["phrase"]])
        phrase_num = p["phrase_num"]
        path.append((phrase, phrase_num, phrase_type))
    return path

def make_noun_phrase(phrase_list, replace):
    noun_phrase_list = []
    noun_flag = 1
    for sur_pos in phrase_list["phrase"]:
        if sur_pos[1] == "名詞" and noun_flag == 1:
            noun_phrase_list.append(replace)
        elif sur_pos[1] == "名詞" and noun_flag == 0:
            noun_phrase_list.append(sur_pos[0])
        if sur_pos[1] != "名詞":
            noun_phrase_list.append(sur_pos[0])
            noun_flag = 0
    noun_phrase = "".join(sorted(set(noun_phrase_list), key=noun_phrase_list.index))
    noun_phrase_and_num = (noun_phrase, phrase_list["phrase_num"],replace)
    return noun_phrase_and_num

#センテンス数を指定.(多すぎるので3に設定してある)
for i in range(3):
    path_list = []
    for n,c in enumerate(sentence_chunk[i]):
        pos_list = [m.pos for m in c.morphs]
        #文中のすべての名詞を含む文節に対して,その文節から構文木の根に至るパスを抽出
        if "名詞" in pos_list:
            surface_pos_list = [(m.surface, m.pos) for m in c.morphs if m.pos != "記号"]
            path = [{"phrase":surface_pos_list, "phrase_num":n}]
            # path = [surface_pos_list]
            dst = int(c.dst)
            while dst != -1:
                surface_pos_list = [(m.surface, m.pos) for m in sentence_chunk[i][dst].morphs if m.pos != '記号']
                path.append({"phrase":surface_pos_list, "phrase_num":dst})
                dst = int(sentence_chunk[i][dst].dst)
            path_list.append(path)
    if len(path_list) <= 1:
        print("WARNING:名詞を含む文節がないor一つしかない")


    #pathの作成
    for phrase_i in range(len(path_list)):
        for phrase_j in range(phrase_i+1, len(path_list)):
            #文節iから根までのpathに文節jがいるかの判定
            if path_list[phrase_j][0]["phrase_num"] in [path_i["phrase_num"] for path_i in path_list[phrase_i]]:
                X_path = [make_noun_phrase(path_list[phrase_i][0], "X")]
                X_path += make_path(path_list[phrase_i][1:],"X")
                X_path = list(filter(lambda x:x[1] < path_list[phrase_j][0]["phrase_num"], X_path))
                X_path = [X_element[0] for X_element in X_path]
                Y = [make_noun_phrase(path_list[phrase_j][0], "Y")[0]]
                XY_path = X_path + Y
                print(" -> ".join(XY_path))
            else:
                X_path = [make_noun_phrase(path_list[phrase_i][0], "X")]
                X_path += make_path(path_list[phrase_i][1:],"X")
                Y_path = [make_noun_phrase(path_list[phrase_j][0], "Y")]
                Y_path += make_path(path_list[phrase_j][1:],"Y")
                XY_path = X_path + Y_path
                XY_path = sorted(XY_path, key=lambda x:x[1])
                ans = XY_path[0][0]
                pre_XY = "X"
                flag = [XY_path[0][1]]
                for XY in XY_path[1:]:
                    if XY[1] in flag:
                        continue
                    if XY[2] == pre_XY:
                        ans += " -> " +  XY[0]
                    else:
                        ans += " | " + XY[0]
                    pre_XY = XY[2]
                    flag.append(XY[1])
                print(ans)

出力

WARNING:名詞を含む文節がないor一つしかない
X | Yのう | 語 -> 研究分野とも -> される
X | Y -> エーアイとは | 語 -> 研究分野とも -> される
X | Yとは | 語 -> 研究分野とも -> される
X | Y -> という -> 道具を -> 用いて -> 研究する -> 計算機科学 -> の -> 一分野を -> 指す | 語 -> 研究分野とも -> される
X | Yと -> 道具を -> 用いて -> 研究する -> 計算機科学 -> の -> 一分野を -> 指す | 語 -> 研究分野とも -> される
X | Y -> という -> 道具を -> 用いて -> 研究する -> 計算機科学 -> の -> 一分野を -> 指す | 語 -> 研究分野とも -> される
X | Yを -> 用いて -> 研究する -> 計算機科学 -> の -> 一分野を -> 指す | 語 -> 研究分野とも -> される
X | Yを -> 研究する -> 計算機科学 -> の -> 一分野を -> 指す | 語 -> 研究分野とも -> される
X | Yする -> 計算機科学 -> の -> 一分野を -> 指す | 語 -> 研究分野とも -> される
X | Y -> の -> 一分野を -> 指す | 語 -> 研究分野とも -> される
X | Yを -> 指す | 語 -> 研究分野とも -> される
                                :
解説

正直,問題を理解する所から苦労しました・・・。言語処理100本ノックのページにサンプルの出力が載っているのでそちらを確認しながら問題を解くと良いと思います.出力したいものとしては特定の文章においてある二つの文節を選択した時(選ばれた二つの文節は同じ根を持つ前提です.)に,下記の二つのパターンがあります.

  • 文節Yが文節Xの経路上に存在する場合,文節Yと交わった以降の文節は根まで同じパスとなる.
  • 文節Yが文節Xの経路上に存在しない場合,根までの経路上で共通する文節が存在し,そこから先は根まで同じパスとなる.

この二つのパターンをコードに起こすと上記のようになります.
まず,forループの前半の部分で48で解いたように名詞を含む文節があった時のその名詞の文節から根までのパスをリストに保持しています.この時,選ばれた文節の名詞句を”X”や”Y”に変換する必要があるのでリストに品詞情報も保持しておきます.また,それぞれの文節間の順番を把握する必要があるため,文節の番号もリストに保持しています.作成したリストを用いて後半のfor文で二つのパターンにおけるpathの作成を行っています.パスの作成時にXやYなどに名詞句を変更するために名詞句を作成し,その名詞句をXやYに変換する関数make_noun_phraseを呼び出します.次に,文節Xと文節Yのパスを作成するために関数make_pathを呼び出します.関数の戻り値としてpath(文節,文節の位置情報,XかYのパスかの三つの情報をtupleにまとめ一つの要素としているリスト)を出力します.最後にXとYの文節の位置関係を確認しながらパスを出力しています.

最後に

今回は第5章の後半の問題を解いていきました.
特に第5章の後半では,文章の大まかの骨格がわかるような構文解析を実施してきました.特定の名詞からパスを辿ることで大まかな文章の構成や内容を把握することができます.一つの文章が長すぎる場合などは今回問題を解いたような形式で確認すると良いかもしれません.
次回は,ついに機械学習の勉強となる第6章を解いていきます.

次の記事

  • 【言語処理100本ノック解いてみた】 第6章前半(整備中)

前の記事

 

コメント

タイトルとURLをコピーしました