簡単で効率的♪ Pythonをつかって、Nグラム表をささっと作成する

前回に引き続き、Nグラムの話です。タイトルをクックパッドぽくしてみました。nグラム表を作って、そこからフレーズを取り出してみます。以下の文献を参考にしました。
長尾眞, 森信介, 1993, 「大規模日本語テキストのnグラム統計の作り方と語句の自動抽出」, 情報処理学会研究報告. 自然言語処理研究会報告 93(61), 1-8
1993年の文献だけあって、「処理能力が向上」、「64MBのメモリ」などなど懐かしさこみあげる文言が踊っています。それだけあって、いかに効率的にやるかという点に焦点があてられています。やはり、人の営為を研ぎ澄ませるのはいつでも制約条件ですね。
まずは下ごしらえです。L文字の文章資源を、i=1,2..文字目からL文字目までのL本の文字列にして、それを辞書順にソート、前後の文字列が何文字目まで同一かを調べます。

from collections import defaultdict

raw_string = お手元の文章資源(ここでは参院選期間中の候補者のツイートを使用)
# 改行コードと半角スペースをなきものにしちゃったけどいいのかな…。
raw_string = raw_string.replace(u"\n",u"").replace(u" ",u"")

# 下ごしらえ
# i文字目からlen(raw_string)文字目までのlen(raw_string)本の文字列をつくる
strings = []
for i in range(len(raw_string)):
	strings.append([raw_string[i:], i])

# 辞書順で並び替え(時間掛かる)
strings = sorted(strings, key=lambda x:x[0])

# i番目の文字列とi+1番目の文字列は、なん文字目まで一致するか?(最大MAX_N文字に設定)
MAX_N = 12
for i in range(len(strings)-1):
	count, _MAX_N = 0, MAX_N
	if len(strings[i][0]) < MAX_N or len(strings[i+1][0]) < MAX_N: # 文字列の長さがMAX_N未満の場合
		if len(strings[i][0]) < len(strings[i+1][0]):
			_MAX_N = len(strings[i][0])-2
		else:
			_MAX_N = len(strings[i+1][0])-2
	for j in range(_MAX_N):
		if strings[i][0][j] == strings[i+1][0][j]:
			count += 1
		else:
			break
	strings[i].append(count)
	#if len(strings[i]) < 3: strings[i].append(count)
	#else: strings[i][2] = count

nグラム表をつくる関数を定義して、試しに10グラム表を出力してみます。論文を読んでいて、ここが一番美しかった!

# nグラム表と、n文字列の左右の1文字辞書(あとで使う)の作成
l_str, r_str = defaultdict(list), defaultdict(list)
def make_ngram_table(raw, strs, n_gram, l_str=l_str, r_str=r_str):
	ngram_table = defaultdict(int)
	for i in range(len(strs)-1):
		if len(strs[i][0]) < n_gram: continue # 文字列の長さがn数未満の場合はスルー
		if strs[i][2] >= n_gram:
			ngram_table[strs[i][0][:n_gram]] += 1 # nグラム表(dict)を作成
			if strs[i][1] != 0: l_str[strs[i][0][:n_gram]].append(raw[strs[i][1]-1]) # 左の1文字辞書へ追記
			r_str[strs[i][0][:n_gram]].append(strs[i][0][n_gram]) # 右の1文字辞書へ追記
	return ngram_table

# 試しに10グラム表を作ってみる
ngram_table = make_ngram_table(raw_string, strings, 10)
# 頻度30回以上に絞り込んで、頻度が高い順にソート
ngram_table_30 = sorted([n for n in  ngram_table.items() if n[1] >= 30], key=lambda x:x[1], reverse=True) 
# 10文字列|頻度 となるように出力
for nstring in ngram_table_30:
	print u"%s|%s" % (nstring[0], nstring[1])
# tp://t.co/|252
# ttp://t.co|252
# http://t.c|252
# (BOT)#石田まさ|124
# OT)#石田まさひろ|124
# BOT)#石田まさひ|124
# す(BOT)#石田ま|85
# 。http://t.|79
# ます(BOT)#石田|66
# ブログを更新しました|45
# ログを更新しました。|45
# ...http://|42
# .http://t.|42
# ..http://t|42
# 』http://t.|40
# す。http://t|37
# グを更新しました。『|36
# ます。http://|31

続いて、n文字列の左右の1文字のレパートリが多い場合に、そのn文字列がひとつの塊である可能性が高いという仮説のもと、先ほど作ったl_str, r_strを利用して、塊の可能性が高いものを出力してみます。

# n文字列の左右の1文字のレパートリが多いとき、単語の可能性が高いと仮定し、それを出力する
def extract_words(ngram_table, freq_thres, lr_thres, l_str=l_str, r_str=r_str):
	for w in ngram_table.items():
		if w[1] >= freq_thres:
			lr_count = len(list(set(l_str[w[0]]))) + len(list(set(r_str[w[0]])))
			if lr_count >= lr_thres:
				print u"%s|%s|%s" % (w[0], w[1], lr_count)



# In [329]: extract_words(make_ngram_table(raw_string, strings, 10), 10, 10)
# Out [329]:
# 単語(候補)|頻度|左右の文字の種類数
# ありがとうございます|24|20
# (BOT)#石田まさ|124|10
# ブログを更新しました|45|30
# tp://t.co/|252|61
# ...http://|42|16
# 』http://t.|40|23
# OT)#石田まさひろ|124|50
# 次の@YouTube|12|12
# りがとうございます。|14|12
# 。http://t.|79|15
# 新しい写真をFace|17|13
# グを更新しました。『|36|15
# http://t.c|252|35
# せていただきました。|12|12

このあと論文は、塊をうまく取り出す方法を幾つか提案しているのですが、それはまた後日ということで今日はここまで。