Pure Go な形態素解析器で実行バイナリに辞書埋め込んだヤツを作ってみた(3) 完結編

ほとんど感想みたいなものですが，ひととおり実装したのでまとめ．
https://github.com/ikawaha/kagome
![demoapp_s.png](https://qiita-image-store.s3.amazonaws.com/0/44487/560680a7-694a-b749-c235-7f3ea771d797.png)

## 前回までのあらすじ

1. [Pure Go な形態素解析器で，辞書同梱のバイナリを作ろうと MeCab辞書を go ソースに変換してコンパイルしたが，](http://qiita.com/ikawaha/items/235371a7b1066ba5ea6c)
2. [コンパイルが非常に遅くなるので，go-bindata で埋め込む形式に変更](http://qiita.com/ikawaha/items/8a94b728105e03a76468)．
3. [未知語処理にも対応しました．](http://qiita.com/ikawaha/items/8c2b3bd56ff427e9d2d0)

## 今回の実装点

* Go っぽいコードを目指して再実装 / テスト追加
* [kuromoji](http://www.atilika.org/) 相当の検索用分割モードを実装 / ユーザー辞書対応
* REST API, Web App のデモ追加

## Go っぽいコードを目指す

最初に書いたコードは，不慣れな部分もあってあんまりGoっぽいコードじゃなかったので，[golint](https://github.com/golang/lint) で指摘される部分を直してみました．eclipse とか使ったらGUIでリファクタリングできるかなと思ってたんですが，どうやら出来なそうだったので，[gofmt を利用してリファクタリング](http://qiita.com/ikawaha/items/73c0a1d975680276b2c7)しました．IDE とか使わなくても十分これでいけそうな気もします．

前のコードは割とモジュール化しようと細かめにパッケージを切っていたのですが，細かくパッケージ切るとコードが書きにくくなることがわかってきたので，必要以上にパッケージには切らないようにしました．

パフォーマンスに関しては，[]byteとstringを相互にキャストしたりすると，コピーが走って遅くなることがわかったので，不用意に[[]byte と string のキャストしないように修正](http://qiita.com/ikawaha/items/3c3994559dfeffb9f8c9)しました．

あと，Go ではテストとカバレッジが取れるようにツールが用意されているので，テストを追加して github に push すると[travis](https://travis-ci.org/ikawaha/kagome) でテストが走り [coveralls](https://coveralls.io/r/ikawaha/kagome) でテストカバレッジが計測できるように連携して，継続的に開発できるように環境整えました．

## kuromoji 相当の検索用分割モードを実装

pure java 形態素解析器の [kuromoji](http://www.atilika.org/) では，検索のインデキシング用に細かめに分割するモードが用意されています．これ相当の分割を行えるようにしました．

* 標準　標準の分割
* 検索　ヒューリスティックの適用によって検索に役立つよう細分割
* 拡張　検索モードに加えて未知語を unigram に分割します（実装してみたけど，これ形態素解析器の仕事なのかちょっと悩む）

|入力内容|標準モード|検索モード|拡張モード|
|:-------|:---------|:---------|:---------|
|関西国際空港|関西国際空港|関西　国際　空港|関西　国際　空港|
|日本経済新聞|日本経済新聞|日本　経済　新聞|日本　経済　新聞|
|シニアソフトウェアエンジニア|シニアソフトウェアエンジニア|シニア　ソフトウェア　エンジニア|シニア　ソフトウェア　エンジニア|
|デジカメを買った|デジカメ　を　買っ　た|デジカメ　を　買っ　た|デ　ジ　カ　メ　を　買っ　た|

kuromoji のコードを読んでみましたが，ヒューリスティックな処理で，漢字のみで構成されている形態素が閾値(2文字)以上なら閾値超えた文だけペナルティを付与するという割と単純な処理でした．kagome での該当コードはこんな感じです．

```go:検索モード
// ペナルティの計算
// searchModeKanjiLength は 2, searchModeKanjiPenalty は 1700 です．
func additionalCost(n *node) int {
	l := utf8.RuneCountInString(n.surface)
	if l > searchModeKanjiLength && kanjiOnly(n.surface) {
		return (l - searchModeKanjiLength) * searchModeKanjiPenalty
	}
	if l > searchModeOtherLength {
		return (l - searchModeOtherLength) * searchModeOtherPenalty
	}
	return 0
}

func (la *lattice) forward(mode tokenizeMode) {
	for i, size := 1, len(la.list); i < size; i++ {
		currentList := la.list[i]
		for index, target := range currentList {
			prevList := la.list[target.start]
			if len(prevList) == 0 {
				la.list[i][index].cost = maximumCost
				continue
			}
			for j, n := range prevList {
				var c int16
				if n.class != USER && target.class != USER {
					c = la.dic.Connection.At(int(n.right), int(target.left))
				}
				totalCost := int64(c) + int64(target.weight) + int64(n.cost)
				if mode != normalModeTokenize {
					totalCost += int64(additionalCost(n)) // ←★ここでペナルティ加えるだけ
				}
				if totalCost > maximumCost {
					totalCost = maximumCost
				}
				if j == 0 || int32(totalCost) < la.list[i][index].cost {
					la.list[i][index].cost = int32(totalCost)
					la.list[i][index].prev = la.list[target.start][j]
				}
			}
		}
	}
	return
}
```

## REST API, Web App のデモ追加

Go には Web App がすぐ作れるという特徴があると思うんですが，形態素解析器がそのままサーバとしてたってくれたらちょっと何かするときに便利かなと思い，コマンドにサーバ機能を追加してみました．

### REST API

`-http `オプションを指定するとWebサーバが立ち上がります．
`localhost` にポート`8080`でサーバを立ち上げた場合，'http://localhost:8080/' に REST でアクセスできます．こんな感じ．

```
$ kagome -http=":8080" &
$ curl -XPUT localhost:8080 -d'{"sentence":"すもももももももものうち"}'
{{
  "status": true,
  "tokens": [
    {
      "id": 36163,
      "start": 0,
      "end": 3,
      "surface": "すもも",
      "class": "KNOWN",
      "features": [
        "名詞",
        "一般",
        "*",
        "*",
        "*",
        "*",
        "すもも",
        "スモモ",
        "スモモ"
      ]
    },
    {
      "id": 73244,
      "start": 3,
      "end": 4,
      "surface": "も",
      "class": "KNOWN",
      "features": [
        "助詞",
        "係助詞",
        "*",
        "*",
        "*",
        "*",
        "も",
        "モ",
        "モ"
      ]
    },
    {
      "id": 74989,
      "start": 4,
      "end": 6,
      "surface": "もも",
      "class": "KNOWN",
      "features": [
        "名詞",
        "一般",
        "*",
        "*",
        "*",
        "*",
        "もも",
        "モモ",
        "モモ"
      ]
    },
    {
      "id": 73244,
      "start": 6,
      "end": 7,
      "surface": "も",
      "class": "KNOWN",
      "features": [
        "助詞",
        "係助詞",
        "*",
        "*",
        "*",
        "*",
        "も",
        "モ",
        "モ"
      ]
    },
    {
      "id": 74989,
      "start": 7,
      "end": 9,
      "surface": "もも",
      "class": "KNOWN",
      "features": [
        "名詞",
        "一般",
        "*",
        "*",
        "*",
        "*",
        "もも",
        "モモ",
        "モモ"
      ]
    },
    {
      "id": 55829,
      "start": 9,
      "end": 10,
      "surface": "の",
      "class": "KNOWN",
      "features": [
        "助詞",
        "連体化",
        "*",
        "*",
        "*",
        "*",
        "の",
        "ノ",
        "ノ"
      ]
    },
    {
      "id": 8024,
      "start": 10,
      "end": 12,
      "surface": "うち",
      "class": "KNOWN",
      "features": [
        "名詞",
        "非自立",
        "副詞可能",
        "*",
        "*",
        "*",
        "うち",
        "ウチ",
        "ウチ"
      ]
    }
  ]
}

```

### Web App デモ

Web サーバを立ち上げた状態で，ブラウザで `/_demo` にアクセスすると，形態素解析のデモ利用できます．
`-http=:8080` を指定した場合，`http://localhost:8080/_demo` になります．Lattice の表示には [graphviz](http://www.graphviz.org/) が必要です．(デモでは Lattice が大きすぎて表示に時間がかかりすぎる場合は Timeout します．そのような場合には lattice ツールの方を試してみてください．)

![demoapp.png](https://qiita-image-store.s3.amazonaws.com/0/44487/26b395d6-327c-55ea-1a5d-322222c58252.png "demoapp.png")


## あとがきというか感想というか

このプログラム，Go を勉強しようと思って始めたんですが，とりあえずの機能はなんとか用意できたかなと云うところでひと段落です．MeCab の辞書ファイルとか，kuromoji のソースとか読んで，ここの処理こうなってたのかとか気づけることも多く，結構勉強になりました．

Go はコードフォーマッタ，テスト，カバレジ，プロファイラと開発に必要なものが一通りそろってて，ストレスなく作業できました．言語的には最近流行の機能とか特にないですが，というか，むしろ機能減ってる気もしますが，「ほー，いいじゃないか，こういうのでいいんだよ」と孤独のグルメ五郎になった気分でつぶやきたくなる嬉しさがあります．とはいえ，こないだ DSIRNLP 勉強会で若い学生さんに聞いたら Go は完全にイロモノあつかいだったのでちょっと寂しかったです．やはりおっさんホイホイな言語なのかもしれません．

ちなみに，なんで kagome(籠目)なのか何人かに聞かれたのですが，由来はカゴの目が Lattice に見えるかなというところです．名前に Go も入ってますしね！