Pythonでスパムフィルタを書く/トークナイズ
さて、スパム判定の前処理として、テキストをトークナイズする処理を書く。
A Plan for Spam では、何をトークンの構成要素としているかというと、
アルファベット、数字、ダッシュ、アポストロフィ、そして$マークを トークンの構成要素とみなしている (おそらくここは改善の余地があ るだろう)。 全て数字のトークンと、HTMLのコメントは無視する。 後者はトークンの区切りともみなさない。
とあるが、これは英語以外の言語を扱うときにあまり現実的とは言えない。
かといって形態素解析とかを自力で実装するのもどうかなと思うので、 ベイジアンフィルタのほかの実装を見てみる。
- Python: ベイジアンフィルタ - Programming Must Go On
- Ruby: ひらくの工房 - Bayesフィルタリングライブラリ
- Ruby: RAA - wakeru
- Scheme: Gauche:SpamFilter
Pythonで書かれてるやつはMeCabを使ってる。 Rubyの実装は漢字とカタカナのみをトークン構成要素とみなして正規表現。 Schemeの実装はBigram?
トークナイズするときの文字エンコードをUTF-8に限定すれば、 Unicode.orgの Blocks.txtと LineBreak.txt を利用して文字種を分けれるぽい。
連続した同一文字種をトークンとするのがなんとなくだけど実装が楽で精度が高そうで、 他言語に簡単に対応できそうなのでそういう方向でいってみよう。
仕様
関数の引数と返り値
モジュール名はpgspam.tokenizerで、 pgspam.tokenizer.execute('words')がリストを返すことにしよう。
オブジェクトのタイプを調べる方法は、object.__class__をチェックするか、 組み込み型限定とするなら、3.6 types -- 組み込み型の名前 で調べるのかな??
Rubyでいうkind_of?みたいのはどこだろ。まあいいか。
と思ったら2chみてたら見つかった。組み込み関数 isinstance() か。
import unittest
import pgspam.tokenizer
class TokenizerTestCase(unittest.TestCase):
def test_execute_return_list(self):
"""pgspam.tokenizer.execute should return list."""
rtn = pgspam.tokenizer.execute('words')
assert isinstance(rtn, list)
Blockを構成しない文字はトークンを区切る
例えば、u'words word2_words3&*words4' という文字列は、 [u'words', u'word2', u'word3', u'word4'] というトークンのリストへと分解される。
def test_space_and_simbol_is_separator(self):
"""docstring for test_separator_is_not_block"""
rtn = pgspam.tokenizer.execute(u'words word2_words3&*words4')
assert [u'words', u'word2', u'word3', u'word4'] == rtn
連続するひらがなとカタカナと漢字を一つのトークンとする
例えば、u'あああ文字列カタカナひらがな&オブジェクト' という文字列は、 [u'あああ', u'文字列', u'カタカナ', u'ひらがな', u'オブジェクト'] というトークンのリストへと分解される。
def test_same_category_is_token(self):
"""docstring for test_separator_is_not_block"""
rtn = pgspam.tokenizer.execute(u'あああ文字列カタカナひらがな&オブジェクト')
assert [u'あああ', u'文字列', u'カタカナ', u'ひらがな', u'オブジェクト'] == rtn
1文字は無視する
def test_space_and_simbol_is_separator(self):
"""docstring for test_separator_is_not_block"""
rtn = pgspam.tokenizer.execute(u'a word_あ&*words4')
assert [u'word', u'word4'] == rtn
トークンに属性をつける
ベイジアンフィルタの改善 で述べられている改善より。
- To, From, Subject, Return-Path, そしてURL内に現われる 単語は、そのように属性がつけられる。例えばSubject行に現われる "foo" は "Subject*foo" となる (アスタリスクは、単語を構成しない文字なら何でも良い)。
例えばURLをトークナイズするときは、'Url*'というprefixをつけてトークナイズしたい。
def test_append_prefix_to_token(self):
"""docstring for test_append_prefix_to_token"""
rtn = pgspam.tokenizer.execute(u'/', u'url*')
assert [u'url*http', u'url*www', u'url*fraction', u'url*jp'] == rtn
noseの日本語のドキュメントが少なくてよくわからんのでいきなりPyUnitを使うように変更しています。 ああ、軟弱だ!!
とりあえずここまでのプロジェクトはタグを切っておいた。