メモ化再帰で始める構文解析

# はじめに

[ScalaMatsuri 2016](http://scalamatsuri.org/)で「[ScalaとSparkによる日本語テキストマイニング](http://www.slideshare.net/wmeddie/scala-matsuri-2016-japanese-text-mining-with-scala-and-spark)」を聞いて面白そうだったので、まずは日本語形態素解析エンジン[kuromoji](https://www.atilika.com/ja/products/kuromoji.html)を触ってみることにしました。
といっても形態素解析するだけなら、ただ単にkuromojiを呼びだすだけなので、もうちょっと賢そうなことをしたいなぁと思っていたところ、「[kuromoji.js使って構文解析した](http://qiita.com/laco0416/items/b75dc8689cf4f08b21f6)」という記事を読みました。
確率自由文脈文法による構文解析……これは何だか凄そうです！

自由文脈文法って何だか聞き覚えあるなと思っていたら、そうそうプログラミング言語でも親しみのあるBNF（[バッカス・ナウア記法](https://ja.wikipedia.org/wiki/%E3%83%90%E3%83%83%E3%82%AB%E3%82%B9%E3%83%BB%E3%83%8A%E3%82%A6%E3%82%A2%E8%A8%98%E6%B3%95)）で記述できる文法のことでした。まぁ単純な置換ルールによる再帰的な文法のことですね。その置換ルールに確率を付けたのが確率自由文脈文法、と。
「[文脈自由文法の話](http://www.slideshare.net/kogecoo/ss-42686327)」というスライドがとても分かりやすかったです。

# 構文解析結果

今回は構文木の推定だけ行って、学習はしていません。
元記事と同じ構文木が得られたので、たぶん合っていると思います。

```
S 0.2 (8.00e-04)
|
+- 名詞句 1.0 (1.00e-01)
|  |
|  +- 名詞 1.0 (1.00e-01)
|  |  |
|  |  +- 形容詞 0.1 (1.00e-01)
|  |  |  |
|  |  |  +- 名詞 - 隣
|  |  |  |
|  |  |  `- 助詞 - の
|  |  |
|  |  `- 名詞 - 客
|  |
|  `- 助詞 - は
|
`- 動詞 0.5 (4.00e-02)
   |
   +- 名詞 1.0 (8.00e-02)
   |  |
   |  +- 形容詞 0.4 (8.00e-02)
   |  |  |
   |  |  +- 副詞 - よく
   |  |  |
   |  |  `- 形容詞 0.2 (2.00e-01)
   |  |     |
   |  |     +- 名詞 - 柿
   |  |     |
   |  |     `- 動詞 - 食う
   |  |
   |  `- 名詞 - 客
   |
   `- 助動詞 - だ
```

# 実装

とりあえず置換ルールのクラスを定義します。

```scala
case class Rule(left: String, right1: String, right2: String, prob: Double)
```

構文木の各ノードは、置換ルールもしくは形態素、そして部分木の確率を持っていれば良さそうです。

```scala
case class PcfgNode(label: Either[Rule, Token], prob: Double)
```

構文木の型は、[scalaz](https://github.com/scalaz/scalaz)のTreeを利用して```Tree[PcfgNode]```とします。

あとは構文木を推定する関数です。元記事で使われているCYK法と内側外側アルゴリズム……はちゃんと勉強したほうが良さそうなので……とりあえずシンプルな再帰関数で実装してみます。
いちおう計算効率にも配慮して、[メモ化](https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%A2%E5%8C%96)してみました。といってもscalazのMemoを噛ませているだけです。scalaz便利。

```scala
def calcPcfgTree: ((Seq[Token], Seq[Rule], String)) => Option[Tree[PcfgNode]] =
  Memo.mutableHashMapMemo { case (inputs: Seq[Token], rules: Seq[Rule], target: String) =>
    inputs.size match {
      case 0 =>
        None
      case 1 =>
        if (inputs.head.getPartOfSpeechLevel1 == target) {
          Some(PcfgNode(Right(inputs.head), 1.0).leaf)
        } else {
          None
        }
      case size =>
        val trees =
          for {
            rule <- rules if rule.left == target
            i <- Range(1, size)
            child1 <- calcPcfgTree(inputs.take(i), rules, rule.right1)
            child2 <- calcPcfgTree(inputs.takeRight(size - i), rules, rule.right2)
          } yield {
            val rootProb = rule.prob * child1.rootLabel.prob * child2.rootLabel.prob
            PcfgNode(Left(rule), rootProb).node(child1, child2)
          }
        trees.reduceOption(Ordering.by((_: Tree[PcfgNode]).rootLabel.prob).max)
    }
  }
```

やりたい気持ちをそのままコードに落とせた気がします。
Scalaのfor文は、複雑なデータ変換もネストを深くせずに書けるのでいいですね。

あとは構文木をprintすればお終いです。
ノードのshow関数を実装すれば、あとはscalazが木っぽく整形してくれます。

```scala
implicit val showPcfgNode = Show.show { node: PcfgNode =>
  node.label match {
    case Left(rule) => f"${rule.left} ${rule.prob} (${node.prob}%.2e)"
    case Right(token) => f"${token.getPartOfSpeechLevel1} - ${token.getBaseForm}"
  }
}

for (r <- result) println(r.drawTree)
```

# 今後の課題

自然言語処理なにそれおいしいの状態でも、なんちゃって構文解析はできることが分かりました。敷居思ったより高くない？
いずれ適当な文章をスクレイピングして、適当な自然言語処理をして、適当にドヤ顔することを夢見つつ、まずは何かしらの教科書読みます。たぶん……。

# 全コード

```scala:src/main/scala/Main.scala
import com.atilika.kuromoji.ipadic.{Token, Tokenizer}

import scala.collection.JavaConverters._
import scalaz.Scalaz.ToTreeOps
import scalaz.{Memo, Show, Tree}

case class Rule(left: String, right1: String, right2: String, prob: Double)

case class PcfgNode(label: Either[Rule, Token], prob: Double)

object Main extends App {
  val rules = Seq(
    Rule("S", "名詞句", "形容詞", 0.5),
    Rule("S", "名詞句", "名詞", 0.3),
    Rule("S", "名詞句", "動詞", 0.2),
    Rule("名詞", "形容詞", "名詞", 1),
    Rule("名詞句", "名詞", "助詞", 1),
    Rule("形容詞", "名詞", "助詞", 0.1),
    Rule("形容詞", "名詞", "動詞", 0.2),
    Rule("形容詞", "副詞", "形容詞", 0.4),
    Rule("形容詞", "副詞", "形容詞", 0.3),
    Rule("動詞", "副詞", "動詞句", 0.5),
    Rule("動詞", "名詞", "助動詞", 0.5)
  )

  val tokenizer = new Tokenizer()
  val tokens = tokenizer.tokenize("隣の客はよく柿食う客だ").asScala.toVector
  val result = calcPcfgTree(tokens, rules, "S")

  implicit val showPcfgNode = Show.show { node: PcfgNode =>
    node.label match {
      case Left(rule) => f"${rule.left} ${rule.prob} (${node.prob}%.2e)"
      case Right(token) => f"${token.getPartOfSpeechLevel1} - ${token.getBaseForm}"
    }
  }

  for (r <- result) println(r.drawTree)

  def calcPcfgTree: ((Seq[Token], Seq[Rule], String)) => Option[Tree[PcfgNode]] =
    Memo.mutableHashMapMemo { case (inputs: Seq[Token], rules: Seq[Rule], target: String) =>
      inputs.size match {
        case 0 =>
          None
        case 1 =>
          if (inputs.head.getPartOfSpeechLevel1 == target) {
            Some(PcfgNode(Right(inputs.head), 1.0).leaf)
          } else {
            None
          }
        case size =>
          val trees =
            for {
              rule <- rules if rule.left == target
              i <- Range(1, size)
              child1 <- calcPcfgTree(inputs.take(i), rules, rule.right1)
              child2 <- calcPcfgTree(inputs.takeRight(size - i), rules, rule.right2)
            } yield {
              val rootProb = rule.prob * child1.rootLabel.prob * child2.rootLabel.prob
              PcfgNode(Left(rule), rootProb).node(child1, child2)
            }
          trees.reduceOption(Ordering.by((_: Tree[PcfgNode]).rootLabel.prob).max)
      }
    }
}
```

```scala:build.sbt
name := "pcfg"
version := "0.0.0"
scalaVersion := "2.11.7"

libraryDependencies ++= Seq(
  "com.atilika.kuromoji" % "kuromoji-ipadic" % "0.9.0",
  "org.scalaz" %% "scalaz-core" % "7.2.1"
)
```

```project/build.properties
sbt.version=0.13.11
```