LoginSignup
11
5

More than 3 years have passed since last update.

日本語の自然文をNeo-Davidsonian形式に変換

Last updated at Posted at 2018-12-21

Linked Ideal代表社員の久保寺です。Nextremerの久保寺です。主に最近は量子アルゴリズムの研究をしておりますが、記号論理学や仮説推論にも興味があり今回は自然言語処理の文脈で記事を書きたいと思います。

背景

今回ご紹介したいのは自然文を簡便な論理式として表現できる1Neo-Davidsonian形式についてです。日本語の自然文を論理式化する方法の一つとしてCCG(組合せ範疇文法)を応用した研究報告があります。今回はCCGによる論理式化ではなく、少々情報量を落としてシンプルな表現として論理式に変換する1Neo-Davidsonian形式についてご紹介すると共に、実際に日本語の自然文を入力として1Neo-Davidsonian形式に変換するプログラムを書いてみます。

1Neo-Davidsonian形式について

1Neo-Davidosonian形式の説明に入る前に、1Neo-Davidosonian形式は一階述語論理の形式になりますので、その辺も補足しながら紹介したいと思います。まずそもそも論理式って何でしょうか?横柄にいえば数学の証明や論理学を記述するための記号列と言えそうです。論理学のルーツは、古代ギリシャにまで遡るそうで、アリストテレスの「オルガノン」という著書群に当時の論理学としてまとめられているそうです。論理学の教科書の中でよく出てくるものにモーダスポネンス(三段論法)があります。下記のような演繹です。

PならばQである(前提1)
Pである(前提2)
Qである(帰結)

論理学では、真偽が決まる(もしくは決められる)主張を「命題」と呼ぶそうです。すなわち、PやQは命題ということになります。これを論理式で書くと下記のようになります。

((P \to Q) \land P)) \vdash Q

ところで、下記のようなものは同じように論理式として書き下せるでしょうか?

人間はいつか死ぬ(前提1)
ソクラテスは人間である(前提2)
ソクラテスはいつか死ぬ(帰結)

この場合「人間はいつか死ぬ」、「ソクラテスは人間である」、「ソクラテスはいつか死ぬ」という単位で真偽が決まるのでそれぞれを命題

P,Q,R

とすると帰結のRをPとQからそのままは導くことができません。人間の解釈であれば、Rの帰結を演繹できそうですが、例えばコンピュータにこれを推論させようとすると、PとQの真偽だけからRを導出することができないからです。

P\land Q \to R???

そこで一階述語論理の出番です。上記の話を含めた詳細は認知システム論 知識と推論(4)一階述語論理がわかりやすいと思いました。一階述語論理では、論理式に下記の要素を導入します。

構成要素 説明
定数 命題ではなく、特定の対象。
変数 命題ではなく、不定の対象。
関数記号 1つ以上の定数もしくは変数を引数とし,1つの定数もしくは変数に対応させる関数。
述語記号 1つ以上の定数もしくは変数を引数とし,真理値(真または偽)に対応させる関数。
全称記号 ∀と変数を用いて、すべてのオブジェクトが満たしている命題をまとめて表現。
存在記号 ∃と変数を用いて、指定された条件を満たすあるオブジェクトが少なくとも1つ存在することを表現。

このような定数、変数、記号を導入することによって、文章を論理式で表現する幅が広がります。例えば、先ほどのソクラテスの件を一階述語論理として記述してみると下記のようになります。

(\forall x Human(x) \to Mortal(x))\land Human(Socrates))\vdash Mortal(Socrates)

さて、日本語の自然文を論理式化する方法の一つとしてCCG(組合せ範疇文法)があります。
例えば田中リベカ, 峯島宏次, Pascual Martínez-Gómez, 宮尾祐介, 戸次大介「日本語CCGパーザに基づく意味解析・推論システムの提案」 言語処理学会第22回年次大会(東北大学)発表論文集, D4-2, 757-760, 2016年.の研究報告があります。上記論文によれば「学生がゆっくり歩いた。」という表現を論理式に書き下すと下記のようになるそうです。
日本語CCGパーザに基づく意味解析・推論システムの提案_図4 .png

今回は効率的な推論処理のための日本語文の論理式変換に向けて稲田和明, 松林優一郎, 井之上直也, 乾健太郎 言語処理学会年次大会発表論文集19th P3-10 2013年の方法による、1Neo-Davidsonian形式をもう少し見てみたいと思います。上記論文で著者らは1Neo-Davidsonian形式を応用して、日本語の自然文を次のような方法で変換する方法を試みています。

  1. 文を、品詞タグ付け、係り受け解析、述語項構造解析、拡張モダリティ解析にかける。
  2. 品詞が助詞以外の品詞を持つ形態素に変数を与える。変数名は単語原型とし、述語項構造解析において、格助詞関係を取る述語には変数 e を、それ以外には変数 x を用いる。
  3. 述語項構造解析の結果から、格関係を表す論理式を加える。
  4. 拡張モダリティ解析の結果から、その解析結果を表す論理式を加える。
  5. 文節と係り受け関係解析、及び助詞から、係り関係の論理式を加える。

例えば、彼らの方法に従うと
「私はケーキを食べる」は、

私(x_1)\landケーキ(x_2)\land食べる(e)\landガ(e,x_1)\landヲ(e,x_2)

「羽田の空港へ行った」

 羽田(x_1)\land空港(x_2)\land行く(e)\landの(x_2,x_1)\landニ(e,x_1)\land相対時(e,非未来)\land真偽(e,成立)

といった具合に変換されます。今回これをプログラムで実装して出力することを目指したいと思います。

KNPについて

Neo-Davidsonian形式に変換するためには、述語項構造解析が必要になります。その点で有名なツールとして京都大学 黒橋・河原研究室で開発されたKNPがあります。KNPに関してわかりやすい記事が愛するKNPの使い方を紹介するKNPの出力を例に格解析の重要性を説明してみるにあります。
KNPをプログラム言語から使うに当たって、perlやpythonのバインディングもあるようです。
最近、Scalaに興味があるので、Scalaのラッパーがないかなーと思って探したところ、何とありました!!!
https://github.com/en-japan-air/scala-juman-knp
Apache License, Version 2.0で提供されています。サンプルのプログラムもあったので
早速使ってみようと思ったのですが、エラーになりました。

サンプルクライアントプログラム
import com.enjapan.knp.KNP

val knp = new KNPCli()
val blist = knp("京都大学に行った。")

blist.root.traverse(println)

error.png
なるほど、KNPCliという型が解決できないのかと。ではそのクラスをimportして再実行。

サンプルクライアントプログラム修正1
import com.enjapan.knp.KNPCli

error2.png

また、エラー、、、今度はrootというメンバ変数が見つけられないと。ソースを読むとcom.enjapan.knp.models.BListにそのメンバ変数があるのでこれもimportして再実行。

サンプルクライアントプログラム修正2
import com.enjapan.knp.models.BList

エラー内容変わらず。。。Scalaって型推論が複雑そうですよね。もう少し情報を与えてあげる必要がありそうです。色々試行錯誤した結果下記のようにBList型にキャストしたら動きました!!!

サンプルクライアントプログラム修正2
import com.enjapan.knp.KNPCli
import com.enjapan.knp.models.BList

object Test2 extends App {

  val knp = new KNPCli()
  val blist = knp("京都大学に行った。")
  val blistObj = blist.getOrElse("").asInstanceOf[BList]
  blistObj.root.traverse(println)
}

result.png

実装についての説明

まず、KNPで解析した時に出力内容が多くてどこに着目したら迷ってしまいます。今回は、述語と格の解析結果が欲しいわけですが、KNPの出力を眺めていると基本的に述語となる文節には、「格解析結果」が出力されるようです。そこでこの出力を捕まえて、1Neo-Davidsonian形式に変換する戦略をとります。traverseメソッドで分析結果を走査できるのでそれに関数を渡してあげれば行けそうです(関数型言語っぽい!)。今回拡張モダリティーの部分はエンハンステーマとして、真偽を特定するものは、否定表現なのかそうでないのかだけ出力するようにします。実際のコードは下記になります。

Neo-Davidsonian出力プログラム
import com.enjapan.knp.KNPCli
import com.enjapan.knp.models.{BList, Bunsetsu}


object NeoDavidsonian extends App {

  var neoDavidsonian = ""   //最終的なneoDavidsonian形式に変換した文字列
  var predicateIndex = 0    //述語の文節を特定するインデックス
  var caseIndex = 0         //格の文節を特定するインデックス
  var caseNominative = ""   //格の表現

  //文節ごとに解析
  def convert(x: Bunsetsu): Unit = {

    //文節はtagのリストとして分析結果が格納されている。
    for (tag <- x.tags) {
      //格解析結果を持っている文節は述語であるようだ。なのでその分析結果の存在有無を確認
      if (tag.features.isDefinedAt("格解析結果")) {
        //格解析結果は述語と格の分析結果がコロンで区切られている
        val caseAnalysis = tag.features.get("格解析結果").getOrElse().toString().split(':')
        predicateIndex += 1
        //格の解析結果はセミコロンで区切られている
        for (y <- caseAnalysis(2).split(';')) {
          //さらに詳細な結果が/で区切りられている
          var caseElement = y.split('/')(0)
          val word = y.split('/')(2)
          if (word != "-") {
            //println(caseElement)
            caseIndex += 1
            if (caseElement.endsWith("ガ")) {
              caseNominative = word
              caseElement = "ガ"
            }
            neoDavidsonian = neoDavidsonian + word + "(x" + caseIndex + ") ∧ "
            neoDavidsonian = neoDavidsonian + caseElement + "(e" + predicateIndex + ",x" + caseIndex + ") ∧ "
          }
        }
        var normalizedSurface = ""
        //述語の表現は、複数で構成されることがありそれは+で区切られる
        if (caseAnalysis(1).contains('+')) {
          val nsElements = caseAnalysis(1).split('+')
          for (element <- nsElements) {
            //読みが/区切りで付与されるのでその情報は落とす
            normalizedSurface += element.substring(0, element.lastIndexOf("/"))
          }
        } else {
          //読みが/区切りで付与されるのでその情報は落とす
          normalizedSurface += caseAnalysis(1).substring(0, caseAnalysis(1).lastIndexOf("/"))
        }
        if (tag.features.isDefinedAt("否定表現")) {
          //否定表現の場合は、述語に「¬」の印をつけておく
          normalizedSurface = "¬" + normalizedSurface
        }
        neoDavidsonian = neoDavidsonian + normalizedSurface + "(e" + predicateIndex + ") ∧ "
      }
    }
  }

  val str = "土星を取り巻く大きな環は、あと1億年もたたないうちに消滅してしまうかもしれない。"
  val knp = new KNPCli()
  val blist = knp(str)
  val blistObj = blist.getOrElse("").asInstanceOf[BList]
  blistObj.root.traverse(convert)
  println(str)
  //最後の記号を落として出力
  println(neoDavidsonian.substring(0, neoDavidsonian.lastIndexOf("∧ ")))

}

結果

まず順当に格や述語がはっきりしているものをテスト
「アリスがボブに手紙を送りました。」

アリス(x_1) ∧ ガ(e_1,x_1) ∧ 手紙(x_2) ∧ ヲ(e_1,x_2) ∧ ボブ(x_3) ∧ ニ(e_1,x_3) ∧ 送る(e_1)

ほぼ期待通り。次は否定表現をテスト
「アリスがボブに手紙を送りませんでした。」

アリス(x_1) ∧ ガ(e_1,x_1) ∧ 手紙(x_2) ∧ ヲ(e_1,x_2) ∧ ボブ(x_3) ∧ ニ(e_1,x_3) ∧ ¬送る(e_1)

これも期待通り。次は二重否定の表現をテスト
「アリスがボブに手紙を送らないわけではない。」やや意味不明ですが、、、

アリス(x_1) ∧ ガ(e_1,x_1) ∧ 手紙(x_2) ∧ ヲ(e_1,x_2) ∧ ボブ(x_3) ∧ ニ(e_1,x_3) ∧ 送る(e_1) 

良い調子です!!!
ここで今日あったニュースの記事から一文入れてみます。
「土星を取り巻く大きな環は、あと1億年もたたないうちに消滅してしまうかもしれない。」

環(x_1) ∧ ガ(e_1,x_1) ∧ うち(x_2) ∧ 時間(e_1,x_2) ∧ 消滅(e_1) ∧ 年(x3) ∧ 時間(e_2,x_3) ∧ うち(x_4) ∧ 外の関係(e_2,x_4) ∧ ¬経つ(e_2) ∧ 環(x_5) ∧ ガ(e_3,x_5) ∧ 土星(x_6) ∧ ヲ(e_3,x_6) ∧ 取り巻く(e_3)

さすがに課題がたくさんありそうです。拡張モダリティーを考慮していないので消滅が真となってます。本来は、「消滅してしまうかもしれない」ということなので、その主観を付記して成立する述語を表現すべきです。拡張モダリティーは、zundaという解析ツールがあるようでこちらも取り入れられれば改善ができそうです。また、一億年という表現が「年」という表現に省略されてしまいました。数量表現にもケアが必要そうです。また文中の「環」という表現はx1とx5に二つ現れました。これは推測ですが、「外の関係」というものと述語が複数になったためかもしれません。これも適切にケアして出力したいところです。

まとめ

拡張モダリティーを付与していないので、限定的ではありますが日本語の自然文を1Neo-Davidsonian形式の論理式として出力することができました。ただしこれ、データ構造としては文節がノードで格が関係のように記述されてます。さらにノードには属性などの情報も付与してもう少しリッチなデータ構造を持たせることもできそうです。なのでグラフ形式で表現するといろいろなアルゴリズムが適用できて面白そうです。このアプローチについても何か記事が書けたらと思います。最後までお読みいただき、ありがとうございました!


  1. Parsons Terence. Events in the Semantics of English. MIT Press, 1990. 

11
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5