読者です 読者をやめる 読者になる 読者になる

golang: goa勉強会を開催しました

golang Go言語 goadesign

connpass.com

goa って何?

goa ってのは golangAPIデザインを書くと,そこから API サーバのモックとかクライアントとかドキュメントとか一通り生成してくれるマイクロサービス用のフレームワークのことです.

goa は golang のソースとして DSL が書けて,そいつを goa にかけてやることでコードを生成します.ぱっと聞いた感じ,最初に抱く印象は「キモい」だと思うんですが,jsonyaml を書いてコードを生成するよりかなり見通しよくAPIがデザインできると思いますし,なにより,goa の生成するコードがすごく読みやすいコードで,使ってるウチにだんだん「goa (キモ)かわいいな」と思うようになってきました.goa 自体のコードも変態的(コード生成プログラムなのでしょうがない)でありながら読みやすいコードで書かれているのも好印象です.

Web APIgolang で書こうと思えば,極論 net/http でも構築できますが,データの受け取りとかバリデーションとか割と同じようなコードを沢山書くことになったり,ちょっとした仕様変更でドキュメント修正したりするのが結構つらいです.

goa 使うと,データの受け取りとかバリデーションの定型的なプログラムはほぼほぼ生成してくれて,同時にドキュメント(仕様)も swagger 形式で生成してくれます.埋めなければならないのは生成されないビジネスロジック部分だけです.一説にはビジネスロジックはプログラムコード全体の20%にも満たないなんて話もありますから,ビジネスロジックに集中できるのは効率の面でも重要です.

goa の開発をおこなってる raphael さんが,gophercon 用に準備した goa のステッカーを twitter にあげていて,遠目に欲しいなーとイイねしたら,ホントにステッカーを送ってもらえることになりまして

海を越えて送ってもらっちゃいました!「送ってあげるからアドレス教えて」と twitter で DM が来て,あまりの嬉しさで舞い上がってしまってメールアドレス教えちゃったのは秘密です.

沢山いただいてしまったので,この機会に goa の魅力を知ってもらおうとステッカー頒布会 兼 goa 勉強会を開催することになりました.

発表

#1

speakerdeck.com

発表はまず,@ikawaha から goa について,簡単なイントロをまとめました. goa のインストールから API デザインの説明一通り.時間があれば Basic認証,テストの話も・・・と, 当初からだいぶ絞ったので20分で普通に終わるかと思ってたんですけど,かなり駆け足で時間もオーバーしちゃいました. はじめて goa 触る人の助けになれば幸いです.

#2

How I create a Microservice using goa - Slideck

@tchssk さんからは goa をつかって実際のサービスを作る話をしていただきました. gorma という goa で使える準標準的なプラグインがあるんですけど,これをつかうと,DB のモデルとかも API デザインから作ってくれます.ずっと気になってたプラグインだったのでホント俺得でした.機会があれば使ってみたい.また,Docker を使ったテストの話や swagger UI の話もありました.goa は API デザインの swagger定義をドキュメントとして生成してくれるので,swagger UI があると,UI 上から作成した Web API の endpoint をポチポチ叩いて確認出来るので,ホントに便利です.

#3

Development using goa and golang on Pacificporter inc.

@haruyama さんからは,実際の現場での goa や golang の Tips をお話しいただきました. goa のミドルウエア使ってセキュリティヘッダ用意する話とか,go の linter を実際どうやって使ってて,どういうところが使い勝手悪いかなど紹介して下さいました.

#4

speakerdeck.com

最後に @dead_cheat さんから「Go のバイナリ配布関する色々」というお題で LT をして下さいました. アセットをバイナリに埋め込むのに go-bindata はよく使ってたんですが,zgok ってのもあるんですね. 発表後,graceful restart の疑問についていくつか twitter で回答も寄せられていたみたいで,やっぱ発表したりするの大事だなと思いました(小並

感想

初めて勉強会なんてものを主催してみたんですが,これは結構大変ですね.今回,質疑応答の時間の時間を全然盛り込んでなく,懇親会も開催しなかったので,いろいろお話ししてみたい方々が集まって下さったのにお声がけできなかったのが非常に心残りでした.

次回,もし勉強会主催することあれば懇親会までなんとか頑張ってみたいものです.

お集まり下さった皆様ありがとうございました!

goa は pr 送ると,申し訳程度の修正でも "great!" とか "perfect!" とか全力で励ましてくれて骨身にしみてありがたい感じになります. issue の中には初心者向けの issue (help wanted beggineres) なんてものもあるので,これを機会に,使うのはもちろん,コントリビュートも是非!

goa.design

それはたぶんあなたの欲しかった名詞ではない

形態素解析 nlp golang Go言語

概要

形態素解析してテキストの中から名詞っぽいところだけを抜き出したい.ってのはよくある話だと思うのですが,単純にやるといろいろ混じってます.

( '-`).oO( そもそも抜き出してるのは名詞の形態素であって,名詞句じゃないもんな・・・.

名詞を抜き出す kagome の単純なサンプル

package main

import (
        "fmt"

        "github.com/ikawaha/kagome/tokenizer"
)

func main() {
        t := tokenizer.New()
        tokens := t.Tokenize("寿司が食べたい。")
        for _, token := range tokens {
                if token.Pos() == "名詞" {
                        fmt.Printf("%s\n", token.Surface)
                }
        }
}

output

$ go run main.go
寿司

以上! 簡単ですね

ところがどっこい

これでいろいろ解析してみると分かると思いますが,おもってたのと違う感じの形態素が取れてたりしないでしょうか?

たとえば,こんなのが入ります.

の,1310,1310,5893,名詞,非自立,一般,*,*,*,の,ノ,ノ
美味しそうなのを見つけた
美味し   形容詞,自立,*,*,形容詞・イ段,ガル接続,美味しい,オイシ,オイシ
そう  名詞,接尾,助動詞語幹,*,*,*,そう,ソウ,ソー
な 助動詞,*,*,*,特殊・ダ,体言接続,だ,ナ,ナ
の 名詞,非自立,一般,*,*,*,の,ノ,ノ
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
見つけ   動詞,自立,*,*,一段,連用形,見つける,ミツケ,ミツケ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS

とか

行ったのは僕だ
行っ  動詞,自立,*,*,五段・カ行促音便,連用タ接続,行く,イッ,イッ
た 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
の 名詞,非自立,一般,*,*,*,の,ノ,ノ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
僕 名詞,代名詞,一般,*,*,*,僕,ボク,ボク
だ 助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ
EOS

連体助詞の「の」ってやつですね.なので,単純にやっちゃうといろいろ混じっちゃいます.

名詞にはどんなのがあるの?

名詞にも色々あります.名詞のカラムの後に続いてる品詞情報がそれです.

辞書によって違いますが,IPADic (kagomeのデフォルト辞書) では,名詞はこんな感じに区分されてます.

IPADic の品詞の説明はこちらの仕様書を御覧下さい. http://www.gavo.t.u-tokyo.ac.jp/~mine/japanese/nlp+slp/NAIST-JDIC_manual.pdf

品詞細分類 おおざっぱな説明
七,千,万
一般 キーボード,暖かみ,饅頭
特殊 そう,そ 例に挙げたこの2つだけ
代名詞 彼,あいつ,何,おれ
非自立 かぎり,ため,まんま,以下 連体詞,「の(格助詞)」,活用語の基本形に接続して使われるもの
サ変接続 解析,大破,ピンぼけ 後ろに「する」「できる」「なさる」「くださる」などをつけられるもの
副詞可能 全員,平日,以来,いつか 曜日,月,量,割合などを表す副詞的な用法を持つ名詞
固有名詞 ノーベル,JR四国,電気通信大学,徳島 人名,地名,組織名など
接続詞的 VS,対,兼 例に挙げた3つのみ
引用文字列 いわく 例に挙げた1つのみ
動詞非自立的 ご覧,ちょ,御覧,頂戴,ちょうだい,ごらん 例に挙げた6つのみ
形容動詞語幹 きらびやか,違法,無限 「きらびやか」な,とか「違法」なとか,ナ形容動詞の活用しない部分
ナイ形容詞語幹 味け,だらし,大人気 ナイ形容詞の活用しない部分

まとめ

用途によりますが,詳細品詞まで見ていらないものは省いた方がよさそうですね. あと,UniDic 使うとまた品詞体系がちょっと異なるので注意が必要です.

ほんとは,形態素を適当な単位につなぎ合わせる文節認定の処理を入れて,名詞句を抜き出したいですね.

たとえば,

15:29 $ kagome
形態素解析にかける
形態素   名詞,一般,*,*,*,*,形態素,ケイタイソ,ケイタイソ
解析  名詞,サ変接続,*,*,*,*,解析,カイセキ,カイセキ
に 助詞,格助詞,一般,*,*,*,に,ニ,ニ
かける   動詞,自立,*,*,一段,基本形,かける,カケル,カケル

なら,「形態素解析」を取り出したいです・・・.

とはいえ,大分類で名詞だけ抜き出しておけば,ゴミ混じっていても特に困らない用途が多いのかもですね.

要するに『形態素抜き出したら,データの中身も見てみてね!』というお話でした.

kagomeでNeologdを無理矢理つかう

golang Go言語 形態素解析

概要

サポートしてるわけでもないし,テストしてるわけでもないんだけど,Hackしてくれるひとがいるみたいだからメモ.なにかあったらフィードバックしてくれるとうれしいです.

個人的な考えですけど,今時点でneologdをサポートするのはちょっと躊躇してます.

理由は

  • Neologdに含まれてるエントリーは英アルファベットがいわゆる半角に寄せられてて mecab 用の辞書と統一が取れてない
  • 短めのエントリーが割とあって,精度に影響が出そうな気がする.適当な長さで切った方がよさそうだけど実験出来てないし,よくわからない
  • カテゴリ分けされてないので地名だけ加えるとかできない
  • めっちゃ長いエントリーとか,断片的な年月日とか不要そうなエントリーが結構ある(解析的な悪さはしないかもだけど

といったところです.でも世の中的には使いたい人も結構いるみたいだからやっぱり対応は考えていきたい.

いいかげんな手順

準備

  • kagome を go get する
  • kagome/cmd/_dictool というディレクトリがあるのでそこで以下を作業する
    • mecab の辞書を持ってきて解凍しておく(以下,フォルダ名は mecab-ipadic-2.7.0-20070801 と仮定)
    • neologd のリポジトリを clone する
    • neologd/seed/mecab-user-dict-seed.YYYYMMDD.csv.xz というファイルがあるので解凍しておく

ビルド

$ go run main.go ipa -mecab mecab-ipadic-2.7.0-20070801 -neologd mecab-ipadic-neologd/seed/mecab-user-dict-seed.20160418.csv

フォルダに ipa.dic というファイルが出来ます(手元では140MBくらいでした). コマンドではファイル名が決め打ちになってしまっているので,適当に名前を変えて下さい.

使う

$ kagome -dic ipa.dic

覚え書き

neologd/seed の中にはいくつかファイルがあるけど,user-dict-seed しか想定してない.適当にマージしてひとつのファイルにしておけばツールに食わせられると思う.

Happy Hacking!

追記

ということなので、mecab-ipadic の方にNeologdのパッチを当ててから辞書をビルドするのが良さそう。(未検証)

callbackでiteratorっぽくcommon-prefix search を直したらちょっとパフォーマンスが改善した

golang Go言語

はじめに

ikawaha.hateblo.jp

これの続編です.callback関数を使うようにして,返値でヒープを使わないようにしたらパフォーマンスもちょっと改善しました.

背景

common-prefix search(共通接頭辞検索)とは「電気」「電気通信」「電気通信大学」というキーワードがあったときに, 入力が「電気通信大学大学院」だったら,これらの「電気」「電気通信」「電気通信大学」という共通の prefix を持ったキーワードを抽出する操作のことです.

形態素解析の辞書引きで,ある位置から始まる形態素の候補をすべて列挙するためにこの操作が実装されているんですが, 抽出されるキーワードごとにキーワードのidと長さを配列にして返していました.

そうすると,この返値の配列のメモリ確保で結構な量が確保されてはGCされるということを繰り返してしまうわけで...

というのが背景です.

golangiterator はない

ないです.channel 使うと一番それっぽくなる気がするんですけど,パフォーマンスは出ないみたいです. 今回は callback を利用する方法で改修してみました.

参考: Iterators in Go

変更点

before

// CommonPrefixSearch finds keywords sharing common prefix in an input
// and returns the ids and it's lengths if found.
func (d DoubleArray) CommonPrefixSearch(input string) (ids, lens []int) {
    var p, q int
    bufLen := len(d)
    for i, size := 0, len(input); i < size; i++ {
        p = q
        q = int(d[p].Base) + int(input[i])
        if q >= bufLen || int(d[q].Check) != p {
            break
        }
        ahead := int(d[q].Base) + int(terminator)
        if ahead < bufLen && int(d[ahead].Check) == q && int(d[ahead].Base) <= 0 {
            ids = append(ids, int(-d[ahead].Base))
            lens = append(lens, i+1)
        }
    }
    return
}

after

// CommonPrefixSearchCallback finds keywords sharing common prefix in an input
// and callback with id and length.
func (d DoubleArray) CommonPrefixSearchCallback(input string, callback func(id, l int)) {
        var p, q int
        bufLen := len(d)
        for i := 0; i < len(input); i++ {
                p = q
                q = int(d[p].Base) + int(input[i])
                if q >= bufLen || int(d[q].Check) != p {
                        break
                }
                ahead := int(d[q].Base) + int(terminator)
                if ahead < bufLen && int(d[ahead].Check) == q && int(d[ahead].Base) <= 0 {
                        callback(int(-d[ahead].Base), i+1)
                }
        }
        return
}

パフォーマンス

before

$ go test --bench .
PASS
BenchmarkAnalyzeNormal-4         10000        222285 ns/op
BenchmarkAnalyzeSearch-4          5000        290241 ns/op
BenchmarkAnalyzeExtended-4        5000        295677 ns/op
ok      github.com/ikawaha/kagome/tokenizer    21.251s

after

go test --bench .
PASS
BenchmarkAnalyzeNormal-4         10000        158133 ns/op
BenchmarkAnalyzeSearch-4         10000        227858 ns/op
BenchmarkAnalyzeExtended-4       10000        235153 ns/op
ok      github.com/ikawaha/kagome/tokenizer    22.270s

alloc_space

$ go tool pprof --alloc_space tokenizer.test fix_mem.prof
Entering interactive mode (type "help" for commands)
(pprof) top
1020.66MB of 1033.17MB total (98.79%)
Dropped 27 nodes (cum <= 5.17MB)
Showing top 10 nodes out of 44 (cum >= 7.50MB)
      flat  flat%   sum%        cum   cum%
  773.95MB 74.91% 74.91%   775.96MB 75.10%  github.com/ikawaha/kagome/tokenizer.Tokenizer.Analyze
  114.31MB 11.06% 85.97%   114.31MB 11.06%  bytes.makeSlice
   60.99MB  5.90% 91.88%    60.99MB  5.90%  strings.genSplit
   46.50MB  4.50% 96.38%    46.50MB  4.50%  encoding/binary.Read
   10.34MB  1.00% 97.38%    32.84MB  3.18%  github.com/ikawaha/kagome/internal/da.Read
    8.98MB  0.87% 98.25%    69.97MB  6.77%  github.com/ikawaha/kagome/internal/dic.NewContents
    3.31MB  0.32% 98.57%    20.31MB  1.97%  github.com/ikawaha/kagome/internal/dic.LoadConnectionTable
    2.28MB  0.22% 98.79%     9.28MB   0.9%  github.com/ikawaha/kagome/internal/dic.LoadMorphSlice
         0     0% 98.79%   117.31MB 11.35%  bytes.(*Buffer).ReadFrom
         0     0% 98.79%     7.50MB  0.73%  encoding/gob.(*Decoder).Decode

ちょっと改善した 🙌

細かすぎて伝わらない「形態素解析器 kagome のメモリ周りの話」を pprof で調べる

Go言語 golang 形態素解析

はじめに

きっかけは形態素解析kagome にいただいた Issue です.

github.com

端的に言うと,

入力文字列に対して,前から1文字ずつずらしながら辞書引きを繰り返して,可能性のある形態素をすべて洗い出すんですが,その際に辞書を CommonPrefixSearch するメソッドでメモリをアロケートしすぎじゃないか?

というお問い合わせです.

たしかに 800MB くらいアロケートしてるように見えます.ひえー

というわけで調査.

道具は揃っている

golang では pprof が使えます.これで速度パフォーマンスとメモリ使用状況が確認できます.

使い方は,KLabGames Tech さんがまとめてくださっているすばらしい記事がありますので,こちらを参照ください.

あと,いくつかの OS バージョンではうまく動かないときがあるので,こっちの Issue も目を通しておくと吉です.

状況の確認

Issue でもらっている状況はこれ.

23:14 $ go test -v ./tokenizer -run=^$ -bench=. -benchmem -benchtime=5s -memprofile=master_mem.prof
PASS
BenchmarkAnalyzeNormal-4       20000        289632 ns/op       19177 B/op        581 allocs/op
BenchmarkAnalyzeSearch-4       20000        390959 ns/op       19177 B/op        581 allocs/op
BenchmarkAnalyzeExtended-4     20000        390214 ns/op       19174 B/op        581 allocs/op
ok      github.com/ikawaha/kagome/tokenizer 32.808s
23:16 $ go tool pprof --alloc_space tokenizer.test master_mem.prof
Entering interactive mode (type "help" for commands)
(pprof) top
1679.93MB of 1689.81MB total (99.42%)
Dropped 23 nodes (cum <= 8.45MB)
Showing top 10 nodes out of 39 (cum >= 11.28MB)
      flat  flat%   sum%        cum   cum%
  810.04MB 47.94% 47.94%  1038.54MB 61.46%  github.com/ikawaha/kagome/internal/dic.IndexTable.CommonPrefixSearch
  410.10MB 24.27% 72.21%  1449.64MB 85.79%  github.com/ikawaha/kagome/tokenizer.Tokenizer.Analyze
  228.50MB 13.52% 85.73%   228.50MB 13.52%  github.com/ikawaha/kagome/internal/da.DoubleArray.CommonPrefixSearch
  114.90MB  6.80% 92.53%   114.90MB  6.80%  bytes.makeSlice
   50.49MB  2.99% 95.52%    50.49MB  2.99%  strings.genSplit
      41MB  2.43% 97.94%       41MB  2.43%  encoding/binary.Read
   10.34MB  0.61% 98.55%    32.84MB  1.94%  github.com/ikawaha/kagome/internal/da.Read
    8.98MB  0.53% 99.09%    59.47MB  3.52%  github.com/ikawaha/kagome/internal/dic.NewContents
    3.31MB   0.2% 99.28%    12.81MB  0.76%  github.com/ikawaha/kagome/internal/dic.LoadConnectionTable
    2.28MB  0.13% 99.42%    11.28MB  0.67%  github.com/ikawaha/kagome/internal/dic.LoadMorphSlice

alloc_space を調べてみると,確かに CommonPrefixSearch() がトップにいます.しかも 800MB くらい alloc してます.

実際のコードのどこで alloc が頻繁に行われてるのかを pprof の list コマンドで知ることが出来ます.

(pprof) list CommonPrefixSearch
Total: 1.65GB
ROUTINE ======================== github.com/ikawaha/kagome/internal/da.DoubleArray.CommonPrefixSearch in /Users/ikawaha/lib/go/src/github.com/ikawaha/kagome/internal/da/da.go
  228.50MB   228.50MB (flat, cum) 13.52% of Total
         .          .    101:       if q >= bufLen || int(d[q].Check) != p {
         .          .    102:           break
         .          .    103:       }
         .          .    104:       ahead := int(d[q].Base) + int(terminator)
         .          .    105:       if ahead < bufLen && int(d[ahead].Check) == q && int(d[ahead].Base) <= 0 {
     110MB      110MB    106:           ids = append(ids, int(-d[ahead].Base))
  118.50MB   118.50MB    107:           lens = append(lens, i+1)
         .          .    108:       }
         .          .    109:   }
         .          .    110:   return
         .          .    111:}
         .          .    112:

なるほど,idslens というリストに要素を追加していく部分でメモリがたくさん確保されてるみたいですね・・・. Issue でも指摘されてるように,idslens は初期容量が 0 なので,追加するたびにメモリが確保されてるんかな・・・

だがしかし

しかし,よく考えると alloc_space は確保されたメモリの総容量で,確保されてGCで回収されてるものも含めての量なのです. ためしに,どのくらいの回数これが確保されてるのか調べてみます.alloc_objects を指定すると調べられます.

23:24 $ go tool pprof --alloc_objects tokenizer.test master_mem.prof
(pprof) list CommonPrefixSearch
Total: 38827588
ROUTINE ======================== github.com/ikawaha/kagome/internal/da.DoubleArray.CommonPrefixSearch in /Users/ikawaha/lib/go/src/github.com/ikawaha/kagome/internal/da/da.go
  13615329   13615329 (flat, cum) 35.07% of Total
         .          .    101:       if q >= bufLen || int(d[q].Check) != p {
         .          .    102:           break
         .          .    103:       }
         .          .    104:       ahead := int(d[q].Base) + int(terminator)
         .          .    105:       if ahead < bufLen && int(d[ahead].Check) == q && int(d[ahead].Base) <= 0 {
   6455404    6455404    106:           ids = append(ids, int(-d[ahead].Base))
   7159925    7159925    107:           lens = append(lens, i+1)
         .          .    108:       }
         .          .    109:   }
         .          .    110:   return
         .          .    111:}
         .          .    112:

ids に注目すると 650万回くらいオブジェクトが生成されてます.ざっくり計算すると,110MB ÷ 650万回 ≒ 18 byte/オブジェクト くらいの量です.

実際メモリはどれくらい使われてるか?

実際の GC で回収されなかったメモリは inuse_space で調べられます.

23:20 $ go tool pprof --inuse_space tokenizer.test master_mem.prof
Entering interactive mode (type "help" for commands)
(pprof) top
104741.70kB of 104741.70kB total (  100%)
Dropped 26 nodes (cum <= 523.71kB)
Showing top 10 nodes out of 36 (cum >= 896.99kB)
      flat  flat%   sum%        cum   cum%
45574.26kB 43.51% 43.51% 45574.26kB 43.51%  strings.genSplit
   32768kB 31.28% 74.80%    32768kB 31.28%  bytes.makeSlice
   10592kB 10.11% 84.91%    10592kB 10.11%  github.com/ikawaha/kagome/internal/da.Read
    9192kB  8.78% 93.68% 54766.26kB 52.29%  github.com/ikawaha/kagome/internal/dic.NewContents
 3388.57kB  3.24% 96.92%  3388.57kB  3.24%  github.com/ikawaha/kagome/internal/dic.LoadConnectionTable
 2329.88kB  2.22% 99.14%  2329.88kB  2.22%  github.com/ikawaha/kagome/internal/dic.LoadMorphSlice
  896.99kB  0.86%   100%   896.99kB  0.86%  reflect.mapassign
         0     0%   100%    32768kB 31.28%  bytes.(*Buffer).ReadFrom
         0     0%   100%   896.99kB  0.86%  encoding/gob.(*Decoder).Decode
         0     0%   100%   896.99kB  0.86%  encoding/gob.(*Decoder).DecodeValue

だいたい 100MBちょっとでしょうか.辞書がだいたい 50MB くらい使用するのが分かってるので,実行に必要なメモリは 50MB くらいだと思われます. そんなに多くはないんじゃないかな・・・と思います.また,CommonPrefixSearch() 由来のメモリはここには現れてないみたいです.リークはしてないんじゃないかな・・・.

速度パフォーマンスを調べてみる

cpuのプロファイルをとってみます.

23:24 $ go test -v ./tokenizer -run=^$ -bench=. -benchtime=5s -memprofile=master_cpu.prof
PASS
BenchmarkAnalyzeNormal-4       20000        287270 ns/op
BenchmarkAnalyzeSearch-4       20000        336573 ns/op
BenchmarkAnalyzeExtended-4     20000        349246 ns/op
ok      github.com/ikawaha/kagome/tokenizer 30.424s
23:26 $ go tool pprof tokenizer.test master_cpu.prof
Entering interactive mode (type "help" for commands)
(pprof) top
111.29MB of 111.29MB total (  100%)
Dropped 36 nodes (cum <= 0.56MB)
Showing top 10 nodes out of 36 (cum >= 1.88MB)
      flat  flat%   sum%        cum   cum%
   52.51MB 47.18% 47.18%    52.51MB 47.18%  strings.genSplit
      32MB 28.75% 75.94%       32MB 28.75%  bytes.makeSlice
   10.34MB  9.29% 85.23%    10.34MB  9.29%  github.com/ikawaha/kagome/internal/da.Read
    8.98MB  8.07% 93.30%    61.48MB 55.25%  github.com/ikawaha/kagome/internal/dic.NewContents
    3.31MB  2.97% 96.27%     3.31MB  2.97%  github.com/ikawaha/kagome/internal/dic.LoadConnectionTable
    2.28MB  2.04% 98.31%     2.28MB  2.04%  github.com/ikawaha/kagome/internal/dic.LoadMorphSlice
    1.88MB  1.69%   100%     1.88MB  1.69%  reflect.mapassign
         0     0%   100%       32MB 28.75%  bytes.(*Buffer).ReadFrom
         0     0%   100%     1.88MB  1.69%  encoding/gob.(*Decoder).Decode
         0     0%   100%     1.88MB  1.69%  encoding/gob.(*Decoder).DecodeValue

ここにも CommonPrefixSeach() は出てきません.パフォーマンス的にみても CommonPrefixSearch() のメモリ確保が問題になっているとは考えにくそうです.

とはいえ確かに効率悪い

CommonPrefixSearch() のコードは下記のようなものなんですが,返値の変数は関数定義のところで宣言されていて,その容量は 0 です. なので,メモリを確保するときは少し多めに確保しておくように修正します.

修正後

func (d DoubleArray) CommonPrefixSearch(input string) (ids, lens []int) {
        const initCapacity = 8
        var (
                p, q  int
                alloc bool
        )
        bufLen := len(d)
        for i, size := 0, len(input); i < size; i++ {
                p = q
                q = int(d[p].Base) + int(input[i])
                if q >= bufLen || int(d[q].Check) != p {
                        break
                }
                ahead := int(d[q].Base) + int(terminator)
                if ahead < bufLen && int(d[ahead].Check) == q && int(d[ahead].Base) <= 0 {
                        if !alloc {
                                ids = make([]int, 0, initCapacity)
                                lens = make([]int, 0, initCapacity)
                                alloc = true
                        }
                        ids = append(ids, int(-d[ahead].Base))
                        lens = append(lens, i+1)
                }
        }
        return
}

これでプロファイルをとってみます.

まず実際の使用量.まぁ,それほど変わりません.

23:35 $ go tool pprof --inuse_space tokenizer.test fixalloc_mem.prof
Entering interactive mode (type "help" for commands)
(pprof) top
114.29MB of 114.29MB total (  100%)
Dropped 24 nodes (cum <= 0.57MB)
Showing top 10 nodes out of 36 (cum >= 1.38MB)
      flat  flat%   sum%        cum   cum%
   56.01MB 49.01% 49.01%    56.01MB 49.01%  strings.genSplit
      32MB 28.00% 77.00%       32MB 28.00%  bytes.makeSlice
   10.34MB  9.05% 86.06%    10.34MB  9.05%  github.com/ikawaha/kagome/internal/da.Read
    8.98MB  7.85% 93.91%    64.98MB 56.86%  github.com/ikawaha/kagome/internal/dic.NewContents
    3.31MB  2.90% 96.81%     3.31MB  2.90%  github.com/ikawaha/kagome/internal/dic.LoadConnectionTable
    2.28MB  1.99% 98.80%     2.28MB  1.99%  github.com/ikawaha/kagome/internal/dic.LoadMorphSlice
    1.38MB  1.20%   100%     1.38MB  1.20%  reflect.mapassign
         0     0%   100%       32MB 28.00%  bytes.(*Buffer).ReadFrom
         0     0%   100%     1.38MB  1.20%  encoding/gob.(*Decoder).Decode
         0     0%   100%     1.38MB  1.20%  encoding/gob.(*Decoder).DecodeValue

で,alloc_space をみてみると・・・.さっきより増えてるー.

23:36 $ go tool pprof --alloc_space tokenizer.test fixalloc_mem.prof
Entering interactive mode (type "help" for commands)
(pprof) top
2424.25MB of 2434.64MB total (99.57%)
Dropped 23 nodes (cum <= 12.17MB)
Showing top 10 nodes out of 37 (cum >= 13.28MB)
      flat  flat%   sum%        cum   cum%
  991.06MB 40.71% 40.71%   991.06MB 40.71%  github.com/ikawaha/kagome/internal/da.DoubleArray.CommonPrefixSearch
  791.54MB 32.51% 73.22%  1782.60MB 73.22%  github.com/ikawaha/kagome/internal/dic.IndexTable.CommonPrefixSearch
  399.54MB 16.41% 89.63%  2183.15MB 89.67%  github.com/ikawaha/kagome/tokenizer.Tokenizer.Analyze
  113.22MB  4.65% 94.28%   113.22MB  4.65%  bytes.makeSlice
   61.99MB  2.55% 96.83%    61.99MB  2.55%  strings.genSplit
      42MB  1.73% 98.55%       42MB  1.73%  encoding/binary.Read
   10.34MB  0.42% 98.98%    29.84MB  1.23%  github.com/ikawaha/kagome/internal/da.Read
    8.98MB  0.37% 99.34%    70.97MB  2.91%  github.com/ikawaha/kagome/internal/dic.NewContents
    3.31MB  0.14% 99.48%    14.81MB  0.61%  github.com/ikawaha/kagome/internal/dic.LoadConnectionTable
    2.28MB 0.093% 99.57%    13.28MB  0.55%  github.com/ikawaha/kagome/internal/dic.LoadMorphSlice

まぁ,そうですよね.多めに確保するんだからこの量は増えますよね.

細かく見てみると・・・

(pprof) list CommonPrefixSearch
Total: 2.38GB
ROUTINE ======================== github.com/ikawaha/kagome/internal/da.DoubleArray.CommonPrefixSearch in /Users/ikawaha/lib/go/src/github.com/ikawaha/kagome/internal/da/da.go
  991.06MB   991.06MB (flat, cum) 40.71% of Total
         .          .    106:           break
         .          .    107:       }
         .          .    108:       ahead := int(d[q].Base) + int(terminator)
         .          .    109:       if ahead < bufLen && int(d[ahead].Check) == q && int(d[ahead].Base) <= 0 {
         .          .    110:           if !alloc {
  503.53MB   503.53MB    111:               ids = make([]int, 0, initCapacity)
  487.53MB   487.53MB    112:               lens = make([]int, 0, initCapacity)
         .          .    113:               alloc = true
         .          .    114:           }
         .          .    115:           ids = append(ids, int(-d[ahead].Base))
         .          .    116:           lens = append(lens, i+1)
         .          .    117:       }

メモリを確保したら append ではメモリ確保が起こってないようですね.

最後にパフォーマンスは?

あんまかわらんかな・・・・.

00:28 $ go test -v ./tokenizer -run=^$ -bench=. -benchtime=5s -memprofile=fixalloc_cpu.prof
PASS
BenchmarkAnalyzeNormal-4       30000        238184 ns/op
BenchmarkAnalyzeSearch-4       20000        316875 ns/op
BenchmarkAnalyzeExtended-4     20000        322532 ns/op
ok      github.com/ikawaha/kagome/tokenizer 34.504s
23:34 $ go tool pprof tokenizer.test fixalloc_cpu.prof
Entering interactive mode (type "help" for commands)
(pprof) top
113446.89kB of 113446.89kB total (  100%)
Dropped 25 nodes (cum <= 567.23kB)
Showing top 10 nodes out of 36 (cum >= 896.99kB)
      flat  flat%   sum%        cum   cum%
54279.45kB 47.85% 47.85% 54279.45kB 47.85%  strings.genSplit
   32768kB 28.88% 76.73%    32768kB 28.88%  bytes.makeSlice
   10592kB  9.34% 86.07%    10592kB  9.34%  github.com/ikawaha/kagome/internal/da.Read
    9192kB  8.10% 94.17% 63471.45kB 55.95%  github.com/ikawaha/kagome/internal/dic.NewContents
 3388.57kB  2.99% 97.16%  3388.57kB  2.99%  github.com/ikawaha/kagome/internal/dic.LoadConnectionTable
 2329.88kB  2.05% 99.21%  2329.88kB  2.05%  github.com/ikawaha/kagome/internal/dic.LoadMorphSlice
  896.99kB  0.79%   100%   896.99kB  0.79%  reflect.mapassign
         0     0%   100%    32768kB 28.88%  bytes.(*Buffer).ReadFrom
         0     0%   100%   896.99kB  0.79%  encoding/gob.(*Decoder).Decode
         0     0%   100%   896.99kB  0.79%  encoding/gob.(*Decoder).DecodeValue

こんな理解であってるのだろうか?長期運用の実績はないので、何か様子がおかしいとかあったら教えてほしいです。突っ込みお待ちしています.

golang で形態素解析を並列実行させて Word Count する

golang Go言語 nlp 形態素解析

はじめに

kagome は goroutine セーフに作ってあるんですが,あんまり並列実行的なサンプルとか書いてないなと思って並列実行でテキストに出てくる名詞を引っこ抜いて数えるサンプルを作りました.

あと,昨日 suzuken =san が 形態素解析をした後の品詞が取り出しにくいよという Issue を上げてくださって,Token に Pos() という品詞を取り出すメソッド追加しました.今までは,

if t := tok.Features(); len(t) > 0 && t[0] == "名詞" {
     // 名詞の時の処理
}

と書いていたのを

if tok.Pos() == "名詞" {
     // 名詞の時の処理
}

と書けるようになりました.suzuken = san ありがとうございます.

( '-`).oO( 品詞以外も便利関数欲しいところですが,辞書によって辞書内容がかなり自由に作れるので,とりあえずは品詞だけの対応になるかなとおもいます.

Word Count

テキストを文区切りして,区切った文ごとに goroutine 呼び出して形態素解析します. 形態素解析結果から名詞だけ取り出して,チャンネルに送り返します.

チャンネルに送り返された語を数えていけば,Word Count のできあがりです.

エラー処理とかが適当なのはご了承ください(^^ゞ

package main

import (
    "bufio"
    "fmt"
    "io"
    "strings"
    "sync"

    "github.com/ikawaha/kagome/splitter"
    "github.com/ikawaha/kagome/tokenizer"
)

const sampleText = `人魚は、南の方の海にばかり棲んでいるのではあ
                    りません。北の海にも棲んでいたのであります。
                    北方の海うみの色は、青うございました。ある
                    とき、岩の上に、女の人魚があがって、あたりの景
                    色をながめながら休んでいました。

                     小川未明作 赤い蝋燭と人魚より`

func nounFilter(ch chan<- string, r io.Reader) {
    var wg sync.WaitGroup
    t := tokenizer.New()
    scanner := bufio.NewScanner(r)
    scanner.Split(splitter.ScanSentences)
    for scanner.Scan() {
        wg.Add(1)
        go func(s string) {
            defer wg.Done()
            tokens := t.Tokenize(s)
            for _, tok := range tokens {
                if tok.Pos() == "名詞" {
                    ch <- tok.Surface
                }
            }
        }(scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        close(ch)
    }
    wg.Wait()
    close(ch)
}

func main() {
    ch := make(chan string, 1024)

    r := strings.NewReader(sampleText)
    go nounFilter(ch, r)

    m := map[string]int{}
    for {
        s, ok := <-ch
        if !ok {
            break
        }
        m[s]++
    }
    for k, v := range m {
        fmt.Printf("%v\t%v\n", k, v)
    }
}

結果はこんな感じになります.

小川   1
作 1
北 1
女 1
方 1
未明  1
海 3
上 1
人魚  3
の 2
景色  1
南 1
北方  1
蝋燭  1
とき  1
岩 1
あたり   1
色 1

形態素解析結果のグラフをスッキリさせてみた

golang Go言語 nlp 形態素解析 heroku

こんなつぶやきを見つけた.

形態素解析で前後になんかあるときと無いときで解析結果が変わるというのは割とよくある話で, 直感的には統一してて欲しいところ何だけど,コストの絶妙な関係で選ばれるノードが変わっちゃうことがあるんですよね.

グラフ書いてみれば分かるんだけど・・・ とグラフ書いてみるとこんなことに.

f:id:ikawaha:20160227163506p:plain

ごちゃごちゃでどうなってるのがぜんぜん分からん(グラフがでかすぎて貼れなかった).

これ,カタカナあると未知語処理が起動して1文字,2文字,3文字… と辞書にないノードを追加しちゃうからなんですよね.

未知語ノードはいっぱい作られるけど,結局は使われなかったりするので,best パスに選ばれなかったらグラフを表示するときには除外するようにしました.

f:id:ikawaha:20160227164039p:plain

めっちゃスッキリ!

f:id:ikawaha:20160227164225p:plain

文脈あるときと比べると何が違いかよく分かりますね! ってよく考えたら web アプリでグラフ書くときタイムアウトしてた処理がこれで軽減できるかも!

ということで,Herokuアプリ の方にも反映させてみました.

golang で neologd の正規化処理を書いてみた(けどダメだった)

golang Go言語 nlp

TL;DR

neologd を使う前にしておいた方がいい入力の前処理があります.

今回はこれを golang でささっと書いてみようと思ってはまって投げ出した話です.

neologd の wiki を参考に必要とされている正規化処理を順に見ていきます.

Regexp.ja · neologd/mecab-ipadic-neologd Wiki · GitHub

基本的には,英数記号は半角にするようになってます.

失敗したコードはこちらにあります.

https://github.com/ikawaha/chits/blob/master/neologd/neologd.go

1. 全角英数字は半角に置換

これは golang の strings.Replacer を定義するだけでささっと書けます. Replacer のいいところは文字列から文字列の変換をただただ書いていくだけでよくて, 濁点とかついた半角カタカナ(複数文字)を全角カタカナ1文字に置き換えるとか自然に書けます. しかも,内部でよしなに trie を作ってくれるみたいなので,たいていの場合はオレオレ実装しなくても効率よくやってくれそうです.

var neologdReplacer = strings.NewReplacer(
    "0", "0", 
    "1", "1", 
    "2", "2", 
    "3", "3", 
    "4", "4",
    ...
}

こんな感じで置換前(全角),置換後(半角)とぐだぐだ並べてくだけで ok です.

置換するときは,neologdReplacer.Replace("hogehoge012") とかして呼び出します.

2. 半角カタカナは全角に変換

これも上と同様に Replacer で.

3. ハイフンマイナスっぽい文字を置換

やっぱり Replacer で.

4. 長音記号っぽい文字を置換

Replacer でしょ.

5. 1回以上連続する長音記号は1回に置換

これはメソッド作って対応.単純に続いてるのがあれば1個に置き換えるだけ.

func (n NeologdNormalizer) ShurinkProlongedSoundMark(s string) string {
    var b bytes.Buffer
    for p := 0; p < len(s); {
        c, w := utf8.DecodeRuneInString(s[p:])
        p += w
        b.WriteRune(c)
        if c != ProlongedSoundMark {
            continue
        }
        for p < len(s) {
            c0, w0 := utf8.DecodeRuneInString(s[p:])
            p += w0
            if c0 != ProlongedSoundMark {
                b.WriteRune(c0)
                break
            }
        }

    }
    return b.String()
}

6.チルダっぽい文字は削除

Replacer は空文字にも置き換えられるのでこれで対応.

7.一部の全角記号を半角に置換

!”#$%&’()*+,−./:;<>?@[¥]^_`{|}

これは,半角に置き換えるように Replacer に追加.

8.一部の半角記号を全角記号に置換

句読点,中黒,カギ括弧は全角にする.もちろん Replacer で出来ます.

9. 空白

ここはちょっと難しいです.(というか出来なかった)

  • 全角スペースは半角スペースに
    • Replacer(ry
  • 解析対象テキストの先頭と末尾の半角スペースは削除
    • strings.Trim() でいけます
  • 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」間に含まれる半角スペースは削除
    • ぉ,おう
  • 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」と「半角英数字」の間に含まれる半角スペースは削除
    • ぉ,おう

空白の削除がむずい

空白除去のサンプルが載っているのでこれをみると,英アルファベットのときは空白取らないで,それ以外で取ればよさそう!

とおもったら,こんなテストケースがサンプルプログラムに付属している!

正規化前 正規化後
南アルプスの 天然水- Sparking* Lemon+ レモン一絞り 南アルプスの天然水- Sparking*Lemon+レモン一絞り

(天然水)- のあとの S(parking) の間の空白は削除しないで,(Sparking)*L(emon) の間の空白は除外するだと!

たしかに用意されてる python のサンプルスクリプトではそうなるんだが,どうやってこれを実現したものか?

サンプルの python スクリプトを参考にさせてもらう

サンプルスクリプトでは unicode カテゴリを定義して処理している.ふむー.

def remove_extra_spaces(s):
    s = re.sub('[  ]+', ' ', s)
    blocks = ''.join(('\u4E00-\u9FFF',  # CJK UNIFIED IDEOGRAPHS
                      '\u3040-\u309F',  # HIRAGANA
                      '\u30A0-\u30FF',  # KATAKANA
                      '\u3000-\u303F',  # CJK SYMBOLS AND PUNCTUATION
                      '\uFF00-\uFFEF'   # HALFWIDTH AND FULLWIDTH FORMS
                      ))
    basic_latin = '\u0000-\u007F'

    def remove_space_between(cls1, cls2, s):
        p = re.compile('([{}]) ([{}])'.format(cls1, cls2))
        while p.search(s):
            s = p.sub(r'\1\2', s)
        return s

    s = remove_space_between(blocks, blocks, s)
    s = remove_space_between(blocks, basic_latin, s)
    s = remove_space_between(basic_latin, blocks, s)
    return s

文字クラス blocks と basic_latin というのが定義されてる. なるほど.これを golangunicode.RangeTable で定義して unicode.In() に指定すればよさそうだ・・・ ・・・って,-* も同じ basic_latin に属するんじゃなかろ(ここで途切れている

どなたかうまく実現できましたら教えてください.

ところで半角に倒すのでいいのかな

neologd は英数アルファベットは半角に倒してるんだけど,mecab はいわゆる全角に倒してるんだよな. 形態素解析に影響しないんだろうか?英数アルファベットが入るような形態素は neologd が網羅しちゃうからいらなくなっちゃうのかな? それか辞書作るときに mecab のリソースの方も半角に変換してるんか?もうちょっと調べてみないとだ・・・

/以上

kagome で UniDic を使えるようにするまでの紆余曲折

golang Go言語 nlp 形態素解析

相変わらずコツコツ作ってる Pure golang形態素解析kagome ですが,これまで IPA 辞書しか使えなかったんですけど,UniDic も使えるようになりました.

バイナリサイズは若干大きくなりますが,辞書内包にしているので,「使ってる辞書何だったけ?案件」がおこりにくく,デプロイも簡単です.

UniDic ?

UniDic は IPA 辞書より形態素単位が細かめに出来ています.また,収録されている形態素数も IPA 辞書より多くなってます.品詞体系も IPA とは若干異なります.細かめに切ってくれるので,検索向きかもしれません.

収録形態素
IPA 392126
UniDic 756463

(左)IPA 辞書では 魏/志/倭/人/伝 となってしまうのが,(右)UniDic だと 魏志/倭人/伝 ととれるように! (嬉しいか?

f:id:ikawaha:20160112112415p:plain:w300f:id:ikawaha:20160112111609p:plain:w300

ただ埋め込むだけ.しかし一筋縄ではいかない

kagome では,辞書を内包するために go-bindata を利用させてもらっています.これを使うと,いわゆるアセットデータをバイナリの中に組み込んで,普通のファイルのようにアクセスできます.古い記事ですが,こんな感じで使えます.go-bindata でコンパイル時にリソースを埋め込んじゃおう! - Qiita.go-bindata を使えば,アセットとして埋め込みたいファイルを 1ファイルの go ソースに変換してくれます.

UniDic も mecab 用にファイルが作られているので,辞書の項目数が若干違うだけで,辞書ビルド用のプログラムを少し変更するだけでビルドして動作するところまでは割とすぐ出来ました.しーかーしー,

$ git push origin feature/unidic_20160104
Counting objects: 24, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (20/20), done.
Writing objects: 100% (24/24), 75.96 MiB | 74.00 KiB/s, done.
Total 24 (delta 12), reused 0 (delta 0)
remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.
remote: error: Trace: f53d265fe46a47bbef042562b153d189
remote: error: See http://git.io/iEPt8g for more information.
remote: error: File internal/dic/data/bindata.go is 215.20 MB; this exceeds GitHub's file size limit of 100.00 MB
To git@github.com:ikawaha/kagome.git
 ! [remote rejected] feature/unidic_20160104 -> feature/unidic_20160104 (pre-receive hook declined)
error: failed to push some refs to 'git@github.com:ikawaha/kagome.git'

アセットファイルが 215M もあって github にあげようとすると拒否られる orz.知っていましたか?github は 50M以上のファイルをアップすると警告されて,100M以上のファイルは push 出来ないことを orz.しかもこれ,ある程度圧縮された状態で 215MB なのです.

「git-lfs を使えよ」と言われるのですが,git-lfs にファイルを置いたら go get 一発でインストールできなくなってしまう・・・.

詰んだ

go-bindata を改造する

golang のプログラムは,同一ディレクトリに入っていれば同じパッケージになるので,ファイルを細かく分割しても問題ないはず.215Mもあるファイルをエディタで切り出したりするのはしんどいので,go-bindata に手を入れました.きれいに分割するのは難しそうなので,とりあえず元ファイルの単位で出力するように手を入れます.

https://github.com/jteeuwen/go-bindata/compare/master...ikawaha:feature/separate_asset_body_20160105

これでもう一度アセット作り直して,これまで 1つだった bindata.go を 12個に分割して push.

-rw-r--r--@  1 john staff   6.5K  1 12 10:09 bindata.go
-rw-r--r--@  1 john staff   2.3K  1 12 10:09 bindata00.go
-rw-r--r--@  1 john staff   6.5M  1 12 10:09 bindata01.go
-rw-r--r--@  1 john staff    15M  1 12 10:09 bindata02.go
-rw-r--r--@  1 john staff    20M  1 12 10:09 bindata03.go
-rw-r--r--@  1 john staff   2.1M  1 12 10:09 bindata04.go
-rw-r--r--@  1 john staff   2.6K  1 12 10:09 bindata05.go
-rw-r--r--@  1 john staff   2.3K  1 12 10:09 bindata06.go
-rw-r--r--@  1 john staff    98M  1 12 10:09 bindata07.go
-rw-r--r--@  1 john staff    33M  1 12 10:09 bindata08.go
-rw-r--r--@  1 john staff    32M  1 12 10:09 bindata09.go
-rw-r--r--@  1 john staff   5.3M  1 12 10:09 bindata10.go
-rw-r--r--@  1 john staff   2.4K  1 12 10:09 bindata11.go
remote: warning: File internal/dic/data/bindata07.go is 97.71 MB; this is larger than GitHub's recommended maximum file size of 50.00 MB

97M ぎりぎりセーフ!

というわけで

kagome で UniDic 使えるようになりました.

欠点は,辞書の初回ロードが遅いと云うことでしょうか...辞書データを圧縮しているので,解凍する時間がどうしても辞書ロード時に必要になってしまうのがオーバーヘッドになっています.辞書データ圧縮しないと速くはなるのですが,github にのせられなくなってしまうので go get 一発でインストールが実現できなくなってしまうという悲しみ.辞書データをもっと細切れにして github に push 出来ればいいのかもしれません.今後の課題です.

次に目指すは Neologd 対応ですかね.内包にするのは難しそうだな・・・.初回にダウンロードしてくるようにするとかだろうか・・・.

そんなわけで今年も紆余曲折していきたいと思います.

github の tag を削除する

メモ

github でタグはつけられるけど,削除するのは手元でやって push するしかない?

git push --delete origin v.1.2.0

みたいにする.

heroku に pure golang な形態素解析サーバをあげてみた

golang Go言語 形態素解析 heroku

はじめに

とりあえずやってみたという内容です.heroku 使うのも初めてなので,用語とか理解とかが違うところもあるかもしれませんが,作業ログがなにかの役に立てば幸いです.ツッコミあればお願いします!

前準備

heroku のアカウントをつくっておく必要があります.なんかずいぶん昔にアカウントだけ作ってあったので,ここの手順は思い出せませんでした. (herokuのトップページ)https://signup.heroku.com/identityから特に詰まるところなく登録できたと思います.

heroku の初期設定

heroku が Toolbelt というユーティリティを用意してくれています.アカウントを作ったら,これを手元にインストールしましょう. インストールできたらコンソールで下記を実行してログインできるか確かめましょう.

$ heroku login
Enter your Heroku credentials.
Email: (← アカウントの email)
Password (typing will be hidden):
Logged in as XXXXX

デプロイするアプリのリポジトリを手元に準備する

heroku にはリポジトリをデプロイします.すでにアプリが github に上がってれば,それを持ってきて,そのリポジトリを heroku に push します. kagome を heroku で動かしたいので, go get github.com/ikawaha/kagome/... してそのリポジトリで作業します.

golang を利用する環境を作る

自分のコードをアップする前に,アプリケーションをどんな環境で heroku で使いたいかを選択します. buildpack というのを heroku が用意してくれているので,この中から選べばいいだけです.

Buildpacks | Heroku Dev Center

今回はもちろん golang なので, https://github.com/heroku/heroku-buildpack-go を使います.コンソールから,

$ heroku create -b https://github.com/heroku/heroku-buildpack-go myapp

とすると,myapp アプリケーションの環境が用意されます.myapp のところは好きに設定できます.指定しないと heroku が適当に決めてくれます. 今回は kagome を動かすので, kagome と設定しました.名前がかぶることは出来ません.あと,無料版で登録できるのは5個までみたいです. 不要なのを登録してしまったら,ダッシュボードのアプリケーションごとの Settings から削除することが出来るので消しちゃいましょう.

$ heroku create -b https://github.com/heroku/heroku-buildpack-go kagome
Creating kagome... done, stack is cedar-14
Buildpack set. Next release on kagome will use https://github.com/heroku/heroku-buildpack-go.
https://kagome.herokuapp.com/ | https://git.heroku.com/kagome.git

これで,heroku のダッシュボードを見てみると,環境が用意されたことが分かります.

f:id:ikawaha:20151218184934p:plain

アプリのアクセス先は,https://kagome.herokuapp.com/ と設定されたみたいです. まだ環境が用意されただけで,中身は空っぽなのでアクセスしてもサービスは起動していませんが, デフォルトのページが表示されると思います.

heroku create がうまくいくと,git のリモートに heroku というのが設定されます.

$ git remote -v
heroku  https://git.heroku.com/kagome.git (fetch)
heroku  https://git.heroku.com/kagome.git (push)
origin  git@github.com:ikawaha/kagome.git (fetch)
origin  git@github.com:ikawaha/kagome.git (push)

heroku 用の設定

リポジトリを heroku に push すると勝手にビルドしてサービスを立ち上げてくれるんですが,そのための設定ファイルがいくつか必要です.

依存関係を解決するためのファイルを用意する

依存関係や golang のバージョンを確認するため(?)に Godep.json が必要です.他のライブラリへの依存関係がなくても必要です.

$ go get github.com/tools/godep

で Godep をインストールして(簡単!)リポジトリ直下で

$ godep save ./...

して下さい.すると,Gedeps というディレクトリが出来るので,これを commit しておきます.

サービスをキックするためのファイルを用意する

Procfile という名前のファイルをリポジトリ直下に置きます.そして,起動するコマンドを記述しておきます. 形態素解析サーバは kagome server -http=:8080 の様に起動するので,これを書けばいいんですが,どのポートを使うかは決め打ちに出来ません.heroku 側が適当に振り出してくれます.振り出されたポートは環境変数 $PORT で参照できるので,

web: kagome server -http=:$PORT

と記述します.web アプリなので,頭に web: というのをつけて登録しています.プロセスタイプは他にも worker とかあるので,気になる方は調べてみて下さい. これも commit しておきます.

デプロイ

heroku にアプリをデプロイします.github に push するのと同じ要領です.まちがえて origin に push しないようにして下さいね(^^ゞ.

git push heroku master

なにか問題があれば途中でこけて push が失敗します.push がうまくいけば,もうアプリが立ち上がっています. heroku ps すると立ち上がってるプロセスを確認できます.

$ heroku ps
=== web (Free): kagome server -http=:$PORT
web.1: up 2015/12/18 18:06:35 (~ 58m ago)

これで,先ほどのアドレスにアクセスすると, f:id:ikawaha:20151218190831p:plain ちゃんと立ち上がってました!

だがしかし・・・

kagomegraphviz を使って解析の課程で出てくる形態素同士の重み付きグラフを表示する機能があるのですが,heroku には graphviz が入ってないので,これが利用できません orz.調べてみると,graphviz が使えるパッケージがありました.

github.com

ありがとうございます!これを使えば解決・・・と思ったけど,これを使うと golang の buildpackage 使えなくない?

heroku-buildpack-multi を使う

heroku 公式で buildpack を複数使うための buildpack が出てます.これを利用すれば ok です.

github.com

アプリの環境を作るところで,$ heroku create -b https://github.com/heroku/heroku-buildpack-go kagome としてたのを $ heroku create -b https://github.com/heroku/heroku-buildpack-multi kagome とします.

これだけだと,golang の環境も,graphviz の環境もインストールされてないので,リポジトリ直下に .buildpackages というファイルを用意して, golang のパッケージと graphviz のパッケージを設定しておきます.

$ cat .buildpacks
https://github.com/heroku/heroku-buildpack-go.git
https://github.com/weibeld/heroku-buildpack-graphviz.git

これを commit して,heroku に push すれば ok です.

f:id:ikawaha:20151218191843p:plain

ちゃんとグラフも表示できました!

golang で勝手にさだベントカレンダー

golang nlp 形態素解析 さだまさし Go言語

はじめに

これは,さだまさし x IT Advent Calendar 2015 - Qiita の17日目の記事ではありません. とても盛り上がってたので,golang で何かして投稿しようと思ったら,すでに埋まってしまっていました orz.

そんなわけで勝手に「さだベントカレンダー」です.

  • 歌詞を載せるのは引用の範囲にとどまるくらいになるようにと思ったので,読みづらいかもしれません.(さだまさしのファンの方なら歌詞をそらんじてらっしゃると期待してます!)
  • 同じような理由でスクレイピングのコードもここには載せないことにしました

ぼくとさだまさし

さだまさしはあんまり知りません.むかし,学生時代にラジオをながらで聞いていた気がします.それも.さだまさしより,アシスタントをしていたマネージャーの方との掛け合いが面白くて聞いていた気がします.そんな訳で書いているうちに申し訳ない気持ちになってきました.

なにかやってみる

さだまさしはあんまり知らないのですが,中島みゆきは割と知ってます.さだまさしの曲をスクレイピングして落としてきて,調べてたら,さだまさし中島みゆきの合作『あの人に似ている』という曲があることを知りました.そこで,この歌詞に出てくる語がどっちの人に由来するかを調べてみることにしました.

語という単位は難しいので,形態素解析器で解析される単位で調査します.

方針

ざっくり見れればいいかなと云うことで,次のような方針で見てみました.

歌詞をどうやって集めるか

歌詞を集めてるサイトからスクレイピングさせてもらいました. goquery を使うと楽ちんです.crawler は A Tour of Goの練習課題にもなってるくらいです. goroutine 便利.

形態素解析

形態素解析には pure golang 形態素解析kagome を使います(ステマ). 使い方はだいたいこんな感じになります.

package main

import (
    "fmt"
    "strings"

    "github.com/ikawaha/kagome/tokenizer"
)

func main() {
    t := tokenizer.New()
    tokens := t.Tokenize("寿司が食べたい。")
    for _, token := range tokens {
        if token.Class == tokenizer.DUMMY {
            // BOS: Begin Of Sentence, EOS: End Of Sentence.
            fmt.Printf("%s\n", token.Surface)
            continue
        }
        features := strings.Join(token.Features(), ",")
        fmt.Printf("%s\t%v\n", token.Surface, features)
    }
}

上のサンプルコードの出力は,

BOS
寿司    名詞,一般,*,*,*,*,寿司,スシ,スシ
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
食べ    動詞,自立,*,*,一段,連用形,食べる,タベ,タベ
たい    助動詞,*,*,*,特殊・タイ,基本形,たい,タイ,タイ
。      記号,句点,*,*,*,*,。,。,。
EOS

こうなります.この要領で1文ずつ処理していけばいいわけです.

名詞とか動詞といった品詞の情報が不要なら,token.Features() を求める必要はありません.今回は,形態素に区切ってその単位の文字列をカウントするだけの簡単な作業にしました.

文区切り

落としてきた歌詞は,空白で区切られていたり改行がまちまちでした.また,歌詞なので句点もありません.

たとえば『あの人に似ている』から少し引用させてもらうと,

昔 哀しい恋をした
街はきれい 人がきれい

その人を 護ってやれなかった
嘘がきれい 誰もがあこがれた

こんな感じです.句点はないけど,1行読み込んで解析単位とすればそれほど変なことにはならなそうです. これを golangbufio.Scanner を使って読み込むと,1行ごとに読み込めますが,空白入りの文字列になってしまいます. そんなときは,kagomeSentenceSplitterbufio.Scanner にセットしてあげることで適当になります.

package main

import (
        "bufio"
        "fmt"
        "strings"

        "github.com/ikawaha/kagome/splitter"
)

var sampleText = `
昔 哀しい恋をした
街はきれい 人がきれい

その人を 護ってやれなかった
嘘がきれい 誰もがあこがれた
`

func main() {

        // 基本設定は https://github.com/ikawaha/kagome/blob/fix/splitter_20151216/splitter/splitter.go#L31
        // これと同じでいいが,今回は改行でも文区切りするようにカスタマイズ
        s := splitter.SentenceSplitter{
                Delim:               []rune{'。', '.', '!', '!', '?', '?', '\n'}, // ここに改行追加する
                Follower:            []rune{'.', '」', '」', '』', ')', ')', '}', '}', '〉', '》'},
                SkipWhiteSpace:      true,
                DoubleLineFeedSplit: true,
                MaxRuneLen:          256,
        }

        scanner := bufio.NewScanner(strings.NewReader(sampleText))
        scanner.Split(s.ScanSentences)
        for scanner.Scan() {
                fmt.Println(scanner.Text())
        }
        if err := scanner.Err(); err != nil {
                panic(err)
        }
}

こうすると,結果は,

昔哀しい恋をした
街はきれい人がきれい
その人を護ってやれなかった
嘘がきれい誰もがあこがれた

とキレイに取れました.これで準備完了です.

さだまさし寄りか,中島みゆき寄りか

形態素ごとにさだまさし寄りか,中島みゆき寄りかを色づけします. 今回は,形態素の出現頻度をカウントしたので,そのカウント数がどちらのアーティストの方が多いかで判定します. しかし,それだけだと,いっぱい曲を書いている方が有利になるので,アーティストごとの全形態素数で割ることにします. 今回は簡単にこいつの比を取って,どっちよりかを判定することにします.そんなんでいいのか?という気もしますが,ネタなのでいいことにします.しました. 100に近ければさだまさし寄り,0に近ければ中島みゆき寄り.50付近なら中立ということにしておきます.

ちなみに,持ってきた歌詞全体で見ると,

アーティスト 曲数 形態素
さだまさし 449 100926
中島みゆき 440 105770

となって,だいたい同じくらいの作曲数に見えます.比較するにはちょうどいい感じです. にしてもすごい曲数.さすが芸歴が長い!

グラフにしてみた

歌詞を載せるのは気が引けたので,グラフにして表示してみました. 青っぽいのがさだまさし寄りで,赤っぽいのが中島みゆき寄りの形態素です.ノードが四角いのはこの曲でだけ使われている形態素です.

f:id:ikawaha:20151216194123p:plain

元の歌詞を見ないと分からないのですが,そこそこ偏りがあります.

特徴的な形態素

この曲でのみ使われている形態素

  • DeJaVu
  • 毀れ
  • のぞかせる
  • 振る舞う
  • 涼し気
  • 話し方

うーん,表記違いで他の曲にも入っている感じもしますけど,どうでしょうか.

それぞれの色が出ている形態素

アーティスト 形態素
さだまさし 護っ,横顔,時折,眼差し,時折,切ない,横顔,時折,眼差し,時折,あこがれ,千,明るく,許し,必ず
中島みゆき ほどか,街.鍵,今夜,逢い,困ら,流れ,夜,なのに,変わっ,なのに,偶然,淋し

さだまさしの方は分からないんですが,中島みゆきの方は確かにこんな感じよなと思いますね(個人の感想です).

後から知ったこと

Wikipedia みてたら,この曲の制作過程が書かれてまして,実は2つのパートを2人がそれぞれ作って合わせたと云うことが分かりました.

この曲は男の歌(さだ担当)・女の歌(中島担当)がそれぞれ同じコード進行の別メロディーで進行し、サビで一緒になるという複雑な構成になっている。これは、当初さだ・中島で作詞・作曲のどちらかをそれぞれ分担するというオファーになっていたものを、さだが「せっかく中島みゆきとやるのだから」と中島に提案し、あえて複雑にしたものである。

なるほど,そう思ってみてみると,それぞれの色が出ている部分がそれぞれのアーティストの担当部分になって・・・そうでもない部分もあってようわからんです. 両方で歌う歌詞は中島みゆきが使ったことがない形態素 時折とか眼差し とかさだまさしの方がよく使う 横顔 なんて形態素が見えるので,さだまさしが作ったのかな—と想像したりしてますが,あなたの判定はどうでしょうか?歌詞と照らし合わせて見てみてください.

お遊びですが,こんな楽しみ方もあると云うことで

おわりに

この曲知らなかったので,買って聞いてみました.いい歌でした.

golang で正規表現の必須要素を抽出してみる

golang Go言語 正規表現

はじめに

この記事は Go Advent Calendar 2015 14日の記事です.

正規表現の必須要素の抽出についてお話ししたいと思います.古典的な話題ですし,鋭い方は golang と別に関係ないんじゃないの?と思われるかもしれませんが,最後に Russ Cox 氏の素晴らしい記事,

にもふれたいと思いますので,最後までおつきあいください.これは,Google Code Search で使われた正規表現のマッチングに関する記事になってます.

この記事が,Russ Cox 氏の記事へのイントロになれば幸いです.

それ,正規表現である必要ありますか?

正規表現は文字列マッチに比べるとどうしても遅いです.しかし,正規表現によっては文字列マッチに落とし込んでから検索できる場合もあります. 極端な例で言えば,apple という文字列も正規表現ですが,これは文字列と等しいので,文字列マッチしたほうがよさそうです.では,apple|orange というのはどうでしょう.これは,appleorange にしかマッチしないので,文字列マッチですみそうな感じがします.

ばっちりマッチする文字列がないときもある

では,(AG|GA)ATA(TT)* というのはどうでしょうか?(TT)* という正規表現は,空文字列にも,TTにも,TTTTにも,TTTTTT・・・にもマッチして,候補は無限に存在します.apple|orange のようにばっちりマッチする文字列を得ることは難しそうです.しかし,よく見るとこの正規表現にマッチする文字列が AGATAGAATA なる文字列から始まることは確実なので,テキストの中から,AGATAGAATA を探し出してから,該当する部分にだけ正規表現のマッチングをかけるようにして,効率化を図ることが出来そうです.

このように,正規表現の中から,マッチングに必須の要素を抜き出すという試みは Gnu Grep でも利用されている古典的な方法です.

ヒューリスティックスなので,実装などはいろいろ工夫のしがいもある面白いアルゴリズムなのですが,ここではなるべく簡単に説明できたらと思います.

正規表現の必須要素を抽出するアルゴリズム

アルゴリズムは,非常にシンプルです.下のアルゴリズムは,みんな大好き Navarro の黄色い本から引用させていただきました.

Flexible Pattern Matching in Strings: Practical On-Line Search Algorithms for Texts and Biological Sequences

Flexible Pattern Matching in Strings: Practical On-Line Search Algorithms for Texts and Biological Sequences

f:id:ikawaha:20151211165743p:plain

なんとなしの解説

詳しいことは Navarro の本を読んでいただければと思いますが,ざっくり説明してみたいと思います.

  • ばっちりマッチする文字列の集合 : All
  • 前部分語であることが分かっている文字列の集合:Prefix
  • 後部分語であることが分かっている文字列の集合:Suffix
  • 正規表現にマッチする文字列には必ず現れる部分文字列の集合:Fact

という4つの集合の組を再帰的に構築していきます.基本的には,All が求まるなら,Prefix と Suffix,Fact は All と同じです(ばっちりマッチするのがあれば,その文字列は,前部分語だし,後部分語だし,部分語でもある).この4つ組を Factor と呼ぶことにします.

Factor は再帰的に定義されます.方針としては,* が使われてるところは,候補が絞れないので,あきらめる.そうでなければ頑張ってみるという感じです.

文字リテラル

文字リテラル a があったとき,Factor は ({a}, {a}, {a}, {a}) です.空文字列の場合も同様です.

選択

x|y と選択があったとき正規表現 xy について,それぞれ Factor X と Factor Y が(再帰的に)計算できるので,x|y の Factor は,それぞれの要素の和を取ったものになります.

つまり,(X.All∪Y.All, X.Prefix∪Y.Prefix, X.Suffix∪Y.Suffix, X.Fact∪Y.Fact)と計算できます.

たとえば,a|b なら,({a},{a},{a},{a})({b},{b},{b},{b}) をあわせて,({a,b},{a,b},{a,b),{a,b}) となります.単純にそれぞれの和を取るだけなので簡単です.

連結

xy と連結されているとき,Factor X と Factor Y が計算できて,xy は,

  • All は,X.All・Y.All
  • Prefix は,X.Prefix を使うか,X.All・Y.Prefix のいずれか良さそうなものを使う
  • Suffix は,Y.Suffix か,X.Suffix・Y.All のいずれか良さそうなものを使う
  • Fact は,X.Fact もしくは,Y.Fact,X.Suffix・Y.Prefix のいずれかから良さそうなものを選んで使う

と計算されます.ここで,集合の連結 は,それぞれの集合の要素を掛け合わせるような演算になります. たとえば,X = {a, b, c}, Y = {x, y, z} なら,X・Y = {ax, ay, az, bx, by, bz, cx, cy, cz} となります.

閉包

x* のとき候補は無数にあって定まりません.このとき特別 Factor を (θ, θ, θ, θ) とします.θ は文字列全体からなる集合と同じような意味で使われていて,候補が定まらないことを意味しています.なので,θにどんな集合を演算しても,θになります.すなわち,θ・A = A・θ = θ∪A = A∪θ = θということです.

わかったような?

アルゴリズムは単純なので,方針は概ね分かると思うのですが,細かいところが実装に任せられています. たとえば,連結の時の「良さそうなのを選んで使う」というやつです.これは,選択肢のどれを使ってもいいんですが,

  • 少なくとも θであるものは使わない方がよさそうです
  • また,集合の要素は少ない方がいいでしょうし(連結でかけ算で増えてくし,最終的なチェックも少なくて済みそう)
  • 集合の要素の一番短い文字列長が大きい方が有利そうです(1文字の要素しか入ってないと,テキスト検索するとき1文字の候補がいっぱいあって結局意味ない可能性もあるので長い文字列が分かった方がよさそう).

用途によっていろいろと工夫しがいがありそうです.

ここでは省略されてますが,文字クラス([a-z]とか)はどのように扱うかとか,+ はどうするかとか,?はどうするかとかもあります. 文字クラスは選択で繋がれた形に展開すれば良さそうですが,あまり数が多いときはθと置き換えてしまってもいいでしょう. + は,かならず1回は要素が現れるので,xx* のように置き換えられます. ? はなくてもいいのでうまく要素が定まりません.θとしてしまっていいでしょう.

細かいところは実装しだいです.

また,連結するときに集合の要素がかけ算で増えてしまうので,Factor が求まっても候補が多すぎて結局使えないということもあります.あんまり数が多くなってきたらθと置き換えてしまうとか,その辺どうするか,とか考えどころです.

なぜ golang か?

前々から,このアルゴリズムで遊んでみたいとは思ってました.仕組みも単純だし,すぐ実装できそうです.アルゴリズムは単純なのですが,正規表現構文木を作るのはとーってもめんどくさい (個人差があります) ので,なかなか手が出なかったです.ところが,golang には regexp/syntax というパッケージがあって,正規表現構文木を直接扱うことが言語的にもサポートされています.なんて素敵!

type Regexp struct {
        Op       Op // operator
        Flags    Flags
        Sub      []*Regexp  // subexpressions, if any
        Sub0     [1]*Regexp // storage for short Sub
        Rune     []rune     // matched runes, for OpLiteral, OpCharClass
        Rune0    [2]rune    // storage for short Rune
        Min, Max int        // min, max for OpRepeat
        Cap      int        // capturing index, for OpCapture
        Name     string     // capturing name, for OpCapture
}

こんな構造体で構成されてます.選択も連結も2項演算ではなくて,a|b|c みたいなときは,Sub に a, b, c が入ってます.細かいところは,writeRegexp(...) というString()メソッドの本体があるので,ここを読むのがわかりやすいです.

遊べるものを用意しました

なかなかうまく説明も出来ないので,簡単なプログラムを作りました.go get して手元で動かして遊んでみてください. サーバとして動かせば,ブラウザ上でアルゴリズムが組み立てる解析木を確認することが出来ます.

github.com

f:id:ikawaha:20151211184301p:plain

Google Code Search での正規表現マッチ

さて,

Regular Expression Matching with a Trigram Index

という Russ Cox 氏の素晴らしい記事についても紹介したいと思います.この記事は,Google Code Search のような沢山のコードの中から正規表現で高速にマッチングを行うにはどうしたらいいか,その手法がまとめてあります.詳しくは記事を読んでいただきたいんですが,おおざっぱに3行で(ひどい)まとめると,

  • 対象ドキュメントは転置インデックスを用いてインデキシングしておく.3-gram を用いる
  • 正規表現の必須要素を計算する.ただし,3-gram の形で抽出する
  • 正規表現の必須要素からクエリを作って,インデックスを検索する

ということです.高速に検索するには,いかに必要ないドキュメントを見ないで済むかということでもあり,対象となるドキュメントを見つけてから実際に正規表現マッチするので高速化できます.そして,このコードは, github.com ここにそろってますので,是非中も見てみてください.

おわりに

今年を振り返ってみると,2月に golang が書きたくて(?)転職して, ikawaha.hateblo.jp

夏には GoCon で発表もさせていただきました. ikawaha.hateblo.jp

ジャストシステムkuromoji.jsid:takuya-a さん,janome@moco_beta さんと一緒に形態素解析の実装についてパネルさせてもらったりしていい経験になりました.

転職してからは golang しか書いてないといえるくらい,golang で楽しく開発させてもらって幸せでした.

来年はなにか新しいネタ考えて遊べるといいなー.

それでは よい(クリスマス|お年)を! ← ここまで読んでいただければFactor が何になるかおわかりですよね :p

追記

こちらにはその昔,UNIX MAGAZINEgrepソースコードを解説した記事がまとまってます.その中に,Factor の抽出方法についての解説もありますのでご参考にどうぞ.

golangで bufio.Scanner を使うだけで日本語の文を1文ずつそれとなく切り出す

golang Go言語 nlp

日本語のテキストから文ぽいところを抜き出すためのプログラムを作りました.

いつも1行ずつ文字列を切り出すときに bufio.Scanner を使っていると思いますが, Scanner は区切りの方法をいろいろ変えることが出来る(標準でもスペースで切り分けられた単語を抜き出すとかある)ので, こいつに文区切り用の関数を設定して,日本語の文書から1文ずつ取り出してみます.

でも,文とは何かとかよく知らないので,概ね句点で区切る感じで取り出します.

利用方法

形態素解析kagome のパッケージに入っているので, kagomego get して使ってください.こんな感じです.

package main

import (
        "bufio"
        "fmt"
        "os"

        "github.com/ikawaha/kagome/splitter" // kagome のパッケージに入ってます
)

func main() {
sampleText := ` 人魚は、南の方の海にばかり棲んでいるのではあ
                     りません。北の海にも棲んでいたのであります。
                      北方の海うみの色は、青うございました。ある
                     とき、岩の上に、女の人魚があがって、あたりの景
                     色をながめながら休んでいました。

                     小川未明作 赤い蝋燭と人魚より`

        scanner := bufio.NewScanner(strings.NewReader(sampleText))
        scanner.Split(splitter.ScanSentences) // ここに足すだけ
        for scanner.Scan() {
                fmt.Println(scanner.Text())
        }
        if err := scanner.Err(); err != nil {
                panic(err)
        }
}

これで

人魚は、南の方の海にばかり棲んでいるのではあ
りません。北の海にも棲んでいたのであります。
北方の海うみの色は、青うございました。ある
とき、岩の上に、女の人魚があがって、あたりの景
色をながめながら休んでいました。

                     小川未明作 赤い蝋燭と人魚より

みたいな文書が,

人魚は、南の方の海にばかり棲んでいるのではありません。
北の海にも棲んでいたのであります。
北方の海うみの色は、青うございました。
あるとき、岩の上に、女の人魚があがって、あたりの景色をながめながら休んでいました。
小川未明作赤い蝋燭と人魚より

という感じで文っぽいものが取れます.句点が振られているようなそこそこちゃんとした形式のテキストならば,それとなく文が切り出せますが, くだけた文章だとそうはいかないのでご注意ください.

基本的な動作

デフォルトの動作.

空白の除去

末尾の改行や,文の間に入っているスペースは除去されます.

区切り文字

区切り文字は,'。', '.', '!', '!', '?', '?' が指定されています. この文字が出てくるとここで文が切れます.

区切り文字に付随することがゆるされる文字

そ,それはこまるな。。。。「びっくりしたよ!!!?」

みたいに区切り文字が連続するときや,括弧が続くときは,これも一緒にくっつけて取ります.

そ,それはこまるな。。。。
「びっくりしたよ!!!?」

となります.付随することが許されている文字は '.', '」', '」', '』', ')', ')', '}', '}', '〉', '》' が指定されています.

改行2回連続で切る

だいたいのケースで,段落の区切りには改行が2回連続するので,改行が2回連続したら区切ります.

最大文長

指定された文字数を超えたら,すみやかに区切ります.末尾に区切り文字や付随する文字が連続する場合には, 指定された文字数を超えることもあります.256文字が指定されています.

カスタマイズして使う

https://github.com/ikawaha/kagome/blob/master/splitter/splitter.go#L33

type SentenceSplitter struct {
    Delim               []rune // delimiter set. ex. {'。','.'}
    Follower            []rune // allow following after delimiters. ex. {'」','』'}
    SkipWhiteSpace      bool   // eliminate white space or not
    DoubleLineFeedSplit bool   // splite at '\n\n' or not
    MaxRuneLen          int    // max sentence length
}

これを設定すると,動作を変更できます.デフォルトの設定はこちらです. https://github.com/ikawaha/kagome/blob/master/splitter/splitter.go#L33

段落っぽいところでくぎる

DelimFollower に何も指定せずに,DoubleLineFeedSplit=true で適当な長さの MaxRuneLen を指定しておくと, 段落っぽいもの(ホンマかいな)が取れると思います.

うまくいかないケース

彼は「どこが文の区切りだろう。」と思った。

みたいなのは,

彼は「どこが文の区切りだろう。」
と思った。

と切れてしまいます.悲しい.あと,モーニング娘。も最後ので切れちゃいます.

まとめ

形態素解析の前処理とかにご利用ください.

モーニング娘。」とかで文が区切れちゃうので,ホントに文区切りしたいなら形態素解析かけた後に処理した方がいいのかもしれませんね. でも形態素解析するために,適当な長さのテキストにしたくて・・・とか悩ましいところですが, なんかひとつサッと使えるものがあるとうまく区切れないところがすぐ分かって必要な処理書きやすいかなと思って作ってみました.


The Go Programming Language (Addison-Wesley Professional Computing) WEB+DB PRESS Vol.82 Foundations of Statistical Natural Language Processing 日本語入力を支える技術 ?変わり続けるコンピュータと言葉の世界 (WEB+DB PRESS plus)

TinySegmenter.jl の高速化手法を追っかけてみた

golang Go言語 nlp

今日の元ネタはこちらです.

chezou.hatenablog.com

他言語との比較が行われているわけですが,Julia 版はアルゴリズムの作りが他とだいぶ変わってしまっているので, そのまま比較するのはどうかなと思うわけですが,でも,Julia が書けるわけでもないので,ざっくりどんな高速化が行われたのか golangで追っかけてみました(結局 golang ネタです).

TinySegmenter はもともと javascript でコンパクトに書かれた分かち書き用のプログラムです.

TinySegmenter: Javascriptだけで実装されたコンパクトな分かち書きソフトウェア

なるべくオリジナルに忠実に golang で実装して,そこから Julia 版の高速化手法を盛り込んでいってパフォーマンスを見ていきます.

で,こちらにご用意したのが golang 版です.

shogo82148.github.io

すごいものを作る方がいらっしゃいますね! perl, ruby, python, c++, c# 各種言語の TinySegmenter を生成してくれます. TeXvim にも対応してます.もちろん golang も.

というわけで,最初のバージョンはこれで生成したものを使わせて頂きます.

Julia 版が行ってる高速化は大まかに2つです

  • セグメントを生成するときに,文字列の連結を行わず,なるべく文字列の添え字の処理だけでおこなう
  • 文字のカテゴリを調べるときに,オリジナルは正規表現を利用して調べているところを,正規表現を使わないで調べる

というわけで,こんな感じでリファクタリングしていきました.

  1. TinySegmenterMaker で生成
  2. 配列の初期値を十分大きく取るように調整
  3. セグメント生成時に文字列の連結などを行わないようにアルゴリズムを修正
  4. 正規表現の利用の排除

パフォーマンスの遷移を調べる

github.com

gobenchui というツールを使うと,コミット履歴ごとにベンチマークを計算してグラフを作ってくれます. 使い方は至ってシンプルで,普段のテストのかわりに

$ gobenchui .

とするだけです.結果はこうなりました.3倍くらい速くなってるでしょうか.正規表現の除去も効いてるみたいですが,やっぱり文字列の連結操作が減ったのが一番効いてるみたいですね.

f:id:ikawaha:20151023155151p:plain

ちなみにメモリパフォーマンスはこんな感じで推移しました.

f:id:ikawaha:20151023155459p:plain

まとめ

Julia 版が高速化前と後でどのくらい違うのか気になるところですが,それはさておき,TinySegmenterMaker がすごすぎます. TinySegmenterMaker に Julia 用のテンプレートを PR したかったのですが,ぼくの Julia 力では無理そうなので,どなたか是非チャレンジしてみて欲しいです.

今回作ったサンプルはこちら.

github.com

追記

model.go という,モデルを切り出したファイルをアルゴリズムの変更の時に作ったんだが,こいつをコミットし忘れてた. 慌ててコミットしたけど,gobenchui による再現がうまくいかないかも.ごめんなさい.