Nグラムを使った未知語の抽出(仮)

n-gramsってどう使うのかよく分かんないなー、どうしてGoogle IMEは「灼眼のシャナ」とか「やはり俺の青春ラブコメはまちがっている。」とかをひとつのフレーズとして認識しているのだろう・・・とググっていたら、こんな論文をみつけた。
森信介, 長尾眞, 1998, 「nグラム統計によるコーパスからの未知語抽出」, 『情報処理学会論文誌』, 39:7, 2093-2100.

「品詞ごとに、前後にくる文字にはパターンがある」という仮定に基いて未知語を探すらしい。
名詞の場合、コーパスを分析すると「ご<名詞>の」とか「、<名詞>し」とかいうパターンが多かった、みたいな。

この論文だと、このパターンの辞書を各品詞について作成したあとに、各単語についても同様のパターンを作成して、なんだか最適化問題を解いているのだけど、まず、各単語についてそれぞれ辞書をつくるほどコストを掛けていたら朝になってしまいそうなので、この論文を数リットルの水で薄めたようなコードを書いてみた。

コーパスには、手元にあった、先の参院選のときの候補者のツイートを使った。
※mecab_libraryについてはこちらを参照してください。

import mecab_library
from collections import defaultdict

corpus = 先の参院選のときの候補者のツイート
tokens = mecab_library.Tokens(corpus.encode("utf-8")).tokens

#In [103]: len(tokens)
#Out[103]: 104666

# 名詞の環境構築
# 名詞の前後にくる文字をひらがなと括弧だけに限定した(reでやるのめんどくさかった)
hiragana = [h for h in u"あぁいぃうぅえぇおぉかがきぎくぐけげこごさざしじすずせぜそぞただちぢつづってでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもやゃゆゅよょらりるれろわゎをん、。「」【】『』()”"]
pos = "名詞"
lr_str = defaultdict(int)
pos_tokens = [token.surface for token in tokens if token.pos == pos]
# 各名詞をコーパスから探して、その前後のひらがなをキーにした辞書を作成(値は頻度)
for pos_token in pos_tokens:
	index = 0
	len_token = len(pos_token.decode("utf-8"))
	_corpus = corpus
	index = _corpus.find(pos_token.decode("utf-8"))
	if index > 0 and index+len_token < len(_corpus) and _corpus[index-1] in hiragana and _corpus[index+len_token] in hiragana:
		lr_str[(_corpus[index-1], _corpus[index+len_token])] += 1
	_corpus = _corpus[index+len_token:]

# なんとなく頻度で並び替えて、前後の文字の組み合わせが生じる確率とか出してみる
lr_sorted = {}
for k, v in sorted(lr_str.items(), key=lambda x:x[1], reverse=True):
	lr_sorted[k] = 1.0*v/len(pos_tokens)

# 左右の文字の組み合わせを使って、それに挟まれた文字列を取り出す
lrs = [(k[0], k[1]) for k, v in lr_sorted.items()]
surfaces = [token.surface.decode("utf-8") for token in tokens]
newwords = defaultdict(int)
for lr in lrs:
	lindex = 0
	_corpus = corpus.replace(u"\n",u" ").replace(u" ",u"").replace(u" ",u"")
	while lindex != -1:
		lindex = _corpus.find(lr[0])
		_corpus = _corpus[lindex+1:]
		r = u""
		i = 0
		# 組み合わせの右の文字に当たるまで一文字ずつ文字列を増やしていく
		while i <= 7 and i < len(_corpus): # 7文字のフレーズまで探すことにした
			r = _corpus[i]
			if i == 0 and r in hiragana: # もうこの辺、ぐっちゃぐちゃ・・・
				break
			if r != lr[1] and r in hiragana: # フレーズにひらがながはいらないようにしちゃった・・・
				break
			if r == lr[1]: # 7文字以内に右側の文字がみつかったよ!
				newword = _corpus[0:i]
				if len(newword) != 1 and newword.find(u"/") == -1 and newword not in surfaces:
					newwords[newword] += 1
				break
			i += 1
# 頻度順に並べ替えて出力(これを確率とかにして閾値とか設定したらいいんじゃないかな・・・)
newwords2 = sorted([(k, v) for k, v in newwords.items()], key=lambda x:x[1], reverse=True)
for newword2 in newwords2[:10]:
	print u"%s\t%s" % (newword2[0], newword2[1])

結果は、↓みたいな感じになった。まぁ、そこまでひどくないけど、ひらがなや括弧を含まないようにしたせいで、微妙なのも混じってしまって、決してよいとは言えない感じだ。それにこれでは、「灼眼のシャナ」とか「やはり俺の青春ラブコメはまちがっている。」はやはり取り出せない・・・。

看護職	22
街頭演説	12
加盟組合	11
目指	10
投票日	10
公式サイト	9
選挙戦	9
一日	9
看護師	8
個人演説会	7
見直	7
夜勤体制	6
定期大会	6
人材確保	6
医療現場	6
看護・介護職	6
利用者	6
義務教育	5

まだまだだなぁ・・・。