統計的手法を用いたspamフィルタ by 武藤武士

spamメールはここで改めて述べるまでもなく、鬱陶しいものです。 特に連絡先をWebなどで知らせればならない立場の人にとっては、 botsによる自動収集でいやがおうにもspamメールは増えていく一方です。 また、メーリングリスト管理者にとっても、 spamメールでメーリングリストのSN比が下がることはユーザの利便性を下げてしまうため、 頭の痛い問題です。

一般的なspamメールフィルタで利用されている 単語(blacklist)を登録する方式のフィルタは、 「お金をもうける」などの確実にはじける対象に対しては有効ですが、 間に*などの文字を入れて別の単語に見せかけるなど、 イタチゴッコになりやすいという側面を持っています。 また、特定のサイトだけを弾いたり、 既知のサイトからのメールだけを受け入れる方法も、 正当なサイト利用者からの正当なメールを弾いてしまう可能性もありなかなか 悩ましいものがあります。

登録する単語が分かってしまい、 それを避けるようにspammerが単語を選ぶのであれば、 これを逐次学習することで、spammerへの対処が可能と考えられるでしょう。 もちろん、明示的な単語リストを逐次更新していくのでもいいですが、 例えばボタン一つでこれはspamメール/広告メールなどと印を付けるだけで、 不要なメールが来なくなれば、メール分類の手間も減らせるでしょう。

今回、ここで紹介するメールフィルタは、従来のルールベースのものではなく、 Naive Bayes Classificationという統計的な学習手法を基礎とした、 逐次学習型のメールフィルタです。

Naive Bayes Classificationによるメールの分類

Naive Bayes Classification(NBC)を利用したメール分類手法に関しては Paul Graham氏の論文 「A Plan for SPAM (http://www.paulgraham.com/spam.html)」 に詳しく説明されています (河合さんによる日本語訳が http://www.shiro.dreamhost.com/scheme/trans/spam-j.htmlで 提供されています。)。 この論文以外にも氏のページからは、 spam対策に関する有用な情報をたくさん手にいれることができます。

NBCを利用したフィルタリングには以下のような特長があります。

詳細は論文に当たってもらうとして、ここではこの手法の概略に関して 説明します。

「Plan for SPAM」の概略

ここでは論文の概略についてのみ説明します。 また、数式などに用いる表記は変更しています。 詳細を知りたい人は論文を実際に読んでみてください。

はじめに、spamであることが分かっているメール群と そうではないメール群を用意します。 ここでは、これらをそれぞれspamメールと良いメールと呼びます。 spamメールと良いメールの数をそれぞれ、 nspamとngood とします。 ここで、ある単語wordの 全てのspamメールと良いメールでの出現回数を、 それぞれ b(word),g(word) とします。 出現回数なので、場合によっては各メールの数よりも大きくなることには 注意をしてください。 これらの値を使うと、ある単語がspamメールに含まれている 確率のようなもの [1] は、

	  b(word)
	  -------
	  nspam  	  (編集注:インラインで組版してください。)
で得られます。 この値が1を越える場合は1とします。 同じようにある単語が良いメールに含まれている確率のようなものは
	  g(word)
	  -------
	  ngood  	  (編集注:インラインで組版してください。)
で計算できます。 これらを使って、以下の式の値を求めます。
	  b(word)
	  -------
	  nspam
p(word)=  -----------------------
	  b(word)     2*g(word)
	  -------  +  ---------
	  nspam           ngood 
	
この値がspamらしさを表す指標となり、 1に近いほどその単語がspamで利用される単語であることを示します。 ここで、良いメール側に2を掛けているのは、 経験的に誤検出( false posivive [2] )を避けるためです。

単語が初めて現れたときにspamらしさをいくつにすべきかという問題が出てきます。 これに関しても、経験的に0.4という中立的な値が割り振られています。

新しいメールが到着したときには、メールを単語トークンに分割し、 もっとも特徴的な15トークンを抽出します。 ここでの特徴的というのは、0.5からどれぐらい離れているか、 つまりどれだけどっちからしいかで評価されます。

これらのトークンから以下の式の値を計算します。

	  prod = Πp(word)

pspam =
	  prod
	  ------------------------------
	  prod + (1-Πp(word))

	  	(編集注:ΠはΠfor all wordで組版
	
この値が文章全体としてのspamらしさを表すことになります。

拡張方法に関して

もう一つ重要な論文に 「Gary Robinson's Rants」 (http://radio.weblogs.com/0101454/stories/2002/09/16/spamDetection.html) があります。 後述のBogofilterや spambayes(http://spambayes.sourceforge.net/)など ではこの改良方式が既に実装され、 コマンドラインオプションで利用可能になっています。 ここでは、「Plan for SPAM」の問題点について概観し、 ad-hocに決められていたパラメータを意味のある値として 決定する方法について述べられています。 今回は、この論文については詳しく述べませんが、 より確率的に妥当な指標を得たい場合はこちらも読んだ方が良いでしょう。

これまで説明した方法は基本的なものですが、 もう少し処理を加えると改良されそうだなと考えられる方法があります。 これらは既にフィルタアプリケーションで実装されているものもありますが、 代表的なものをここで紹介します。

  • 複数語を単語とみなす。

    慣用句などのように、複数の単語が集まって、 初めて一つの意味を持つ場合が存在します。 spamメールの例でいくと、 「未承諾広告」は3単語ではじめてspamらしさを表現すると言ってもいいでしょう。 このような単語を扱うために、 2語語句などの出現確率を利用する方法が考えられます。

  • メールヘッダの一部だけを利用する。

    メールヘッダには

  • 確率バイアスがかからないように、メールのユニークさを保証する。

    spamメールは一般に同じ内容が様々なアドレスに対して送られます。 みなさんも各種メーリングリストから同じ内容のspamメールが 次々に送られてきて、辟易したご経験があることでしょう。 これらを単純にspamとして処理すると、 確率的にバイアスがかかってしまいます [3]。 そこで、同じ内容のメールは1度と数えるような改良が考えられます。 このためには、メッセージIDを利用したり、 本文部分のMD5を利用することができます。 spamメールが複数のSMTPサーバから送られることが多いことを考えると、 後者の方が有効だと思われます。

  • 分類項目をspamメールと良いメールだけにせず、 もっと複数の分類を扱う。

    後述のifileがまさにこのためのメールフィルタです。 一般にメーリングリストなどはメールヘッダをうまく利用することで 分類可能ですが、 例えば 「メーリングリストを問わず、Lispに関する記事を一つのフォルダに集めたい」とか、 「緊急で処理する必要のあるものだけ集めたい」という要望には このような複数クラスでの分類が可能であった方が助かります。

ソフトウエア紹介

ここでは、論文の筆者のサイトの一覧 (http://www.paulgraham.com/filters.html) から、主にports collectionに含まれているソフトウエアに関して説明します。

表1に主にportsに含まれているNBCを用いたspamメールフィルタを示します。 かなりの数がありますが、最初から日本語が利用できる状態になっているものは ありません。

表 1. portsに含まれているNBC spamフィルタの一覧

名称バージョン開発サイトportsの場所記述言語備考
Bayespam0.9.2http://www.garyarnold.com/projects.php#bayespammail/bayespamPerlスクリプト 
SpamOracle1.2http://cristal.inria.fr/~xleroy/software.htmlmail/spamoracleOcaml 
SpamProbe0.7chttp://sourceforge.net/projects/spamprobe/mail/spamprobeC++ 
ifile1.1.4http://www.nongnu.org/ifile/mail/ifileC(+Tcl,)spamかそうでないかだけではなく、複数のクラスに対応。
Bogofilter0.8.0_2http://bogofilter.sourceforge.net/mail/bogofilterC 
Mozilla1.3ahttp://www.mozilla.org/www/mozilla-devel(予定)C++1.3a以降でspamフィルタを組み込み。
Gauche:SpamFilter http://www.shiro.dreamhost.com/scheme/wiliki/wiliki.cgi?Gauche%3aSpamFilter&l=jpなしGauche将来的にはscmailとの連携も予定。

表2に各フィルタの機能比較を示します。 MUAで対応しているMozillaを除いては、 基本的にprocmailなどのMDAから利用するように設計されています。 ifileはデータベースがテキストファイルなので、 単語分布が簡単に確認でき、面白いです。

表 2. 各フィルタの機能比較

ソフトウエア ドキュメント [a] データベースファイル/ディレクトリ(default)データベース形式 procmail対応 [b] 備考
BayespamREADME指定が必須(-oオプション)BerkleyDB v.3o 
SpamOracleREADME~/.spamoracle.db独自バイナリo 
SpamProbeman~/.spamprobe/BerkleyDBo 
ifileman~/.idataPlainText- 
Bogofilterman,READMEなど多数~/.bogofilter/BerkleyDBo 
Mozillahttp://www.mozilla.org/mailnews/spam.html  xMUA組み込みの独自インターフェース。
注意:
a. manはmanpage, 他はその名称のファイル。
b. o:レシピがドキュメントに存在。 -:レシピの記述無し。

ここでは、詳細な利用法は述べませんが、 基本的な操作に必要なコマンドラインオプション一覧を 表3に示します。 詳細は各フィルタ付属のドキュメントを参照してください。

表 3. コマンドラインオプション一覧

ソフトウエアデータベースの指定spamメール/フォルダの指定良いメール/フォルダの指定学習分類テスト備考
Bayspam-o database.db(必須)--spam file_or_directory--good file_or_directorybayes_process_email.plを利用  
SpamOracle -spam spammails-good goodmailsspamoracle add -good goodmails -spam spammailsspamoracle test testmails 
SpamProbe-d database_directoryspam spammailsgood goodmailsspamとgoodで個別にspamprobeを実行して学習。spamprobe score testmails 
ifile-b database_file-i spam_folder-i good_folder各カテゴリ毎に、ifile -i category_folderifile -c -q 
Bogofilter-d database_directory-s-n各メール毎にcat spammail|bogofilter -s [or -n]cat testmail|bogofilter -vメールの入力は標準入力から。

実験

今回紹介したソフトウエアはまだ日本語への対応などがされていないもので、 すぐに利用できるようなものではありません。 しかし、筆者が受け取るspamメールの大多数は英語のもので、 ついで韓国語や中国語の順ですので、一見spamメールの分類を作るには 問題が無さそうに思えます。 しかし、分類をする以上はspamメールで無い例も必須になりますので、 単語が空白で区切られていることを仮定している [4] これらのソフトウエアを利用しても、ヘッダの情報が有効に利用される程度で 効果が半減してしまいます。

日本語で分かち書きが可能なソフトウエアとしては、 kakasi (japanese/kakasi, http://kakasi.namazu.org/) やChasen (japanese/chasen, http://chasen.aist-nara.ac.jp/: 本来は形態素解析のためのソフトウエア) があります。 ここでは、kakasiを使って単語を分割して、 学習する実験をしてみました。 ここでは、改造の容易さを優先して、 PerlスクリプトであるBayespamを利用します。

図1に学習用プログラムの変更箇所を示します。 本来であればkakasiのPerlモジュール版である Text::Kakasi (japanese/p5-Text-Kakasi)を利用した方が 速度面でも有利なのですが、 ここでは実験と言うことで簡単な実装にとどめています。 「単語」の意味もオリジナルと変っているのには注意してください。

図 1. bayes_process_email.plへの変更部分

--- /usr/local/bin/bayes_process_email.pl	Thu Jan  2 00:11:55 2003
+++ bayes_process_email.pl	Thu Jan  9 01:57:01 2003
@@ -232,7 +232,7 @@
 	$mailcount++;
 	print '.' if ( $mailcount % 100 ) == 0;
 
-	if ( ( $file eq "-" ) || open(F, "< $file") )
+	if ( ( $file eq "-" ) || open(F, "/usr/local/bin/nkf -e $file|/usr/local/bin/kakasi -w|") )
 	{
 		my $data;
 		$data = $parser->parse(\*F) if $file ne "-";
@@ -242,12 +242,15 @@
 
 		# Rip out HTML comments
 		$message =~ s/\<\!\-.*\-\>//gs;
-		$message = lc $message if not $case_sensitive;
 
-		foreach (split( /[^\w'$-]+/, $message ) )
+		foreach (split( /\s+/, $message ) )
 		{
 			# Ignore all-numeric tokens
-			if ( !( $_ =~ /^[0-9\-]+$/g ) ) { if (length($_) > 0){$token_occurrences{ $_ } += 1.0;} }
+		  if ( !( $_ =~ /^[0-9\-]+$/g ) ) { 
+		    if (length($_) > 0){
+			$token_occurrences{ $_ } += 1.0;
+		    } 
+		  }
 		}
 	}
 }

実験対象のデータは、spamメール626通、良いメール802通となっています。 良いメールは本誌記者用メーリングリストを利用しましたので、 出現単語などにはかなり偏りがあります。 筆者が目でみて判断したspamメール(555通)にはほとんど日本語のspamが含まれなかったため、 無料プロバイダなどのサービスを利用するときに自動的に購読しなくてはならない 宣伝が中心のものを選択し、71通追加してあります。

このプログラムで単語と認識されたものは全部で14572になります。 学習したデータベースを 図2 のプログラムで調べると、以下のような傾向が見られます。

spamメールに関しては、現在利用していない古いアドレスから 入手したものがほとんどです。 その他のメールは現在のアドレスとなっています。 このため、前者のspam度は0.99付近と大きいのに比べて、 後者のspam度は0.1前後と小さくなっています。

図 2. データベースから単語一覧を表示するスクリプト

#!/usr/bin/perl -w

use strict;
use MIME::Parser;
use Fcntl;
use DB_File;

use vars qw($number_of_messages %token_occurrences);
use Bayespam::Process;

my $number_of_nonspam_messages = 0;
my $number_of_spam_messages = 0;
my %token_ratings = ();

my $outfile = shift;

my $parser = new MIME::Parser;

$parser->output_to_core(1);
$parser->extract_uuencode(1);

# Open the database
tie( %token_ratings, "DB_File", $outfile, O_CREAT|O_RDWR, 0666, $DB_BTREE) ||
	die "Could not open or create database $outfile.";

# Disable STDOUT buffering
$| = 1;

# Read in saved settings in database
$number_of_spam_messages = $token_ratings{ " spam " } || 0;
$number_of_nonspam_messages = $token_ratings{ " nonspam " } || 0;

# print "SPAM#:$number_of_spam_messages, NON_SPAM#:$number_of_nonspam_messages\n";
foreach (%token_ratings) {
  next if(!defined $token_ratings{$_});
  my(@line)=split(/\s+/,$token_ratings{$_});
  print "$_\t@line\n" ;
}

untie( %token_ratings );

おわりに

今回は、日本語の対応が完全なソフトウエアの紹介と言うわけではなかったため、 すぐに役に立つという記事にはなりませんでした。 申し訳ありません。 ただし、日本語化などの対応や、 Mozillaでの実装次第ではまもなく このような手法も利用されていくようになってくるのでしょう。 問題は、ここで紹介したソフトウエアはMozillaの実装を除いては、 古き良きUNIXの伝統に従ったフィルタでの実装となっていることです。 単なるユーザにとっては、 MUAからいかに便利に利用できるかというインターフェース回りで 工夫が欲しいところだと思います。 我こそはと思わん方は是非MUAへの組み込みにも挑戦してみてください。

注意

[1]

厳密な確率にはならないことは、 値が1を越えることから直感的に分かると思います。

[2]

「spamでないものをspamと判断してしまうこと。」 日本語では第一種の誤りともいいます。 ここでは、分かりやすさを優先して河合さんの訳からこれを採用しました。 同じく、false negative、 つまり「spamであるのにspamと判断されなかったもの」は 「逃がすメール」としました。

[3]

もっとも、しつこいspamメールはバイアスをかけて 積極的に排除するという方針もあり得ます。

[4]

中にはASCIIコードであることを単語の構成要素として、 しっかりチェックしているソフトウエアもあります。

[5]

ひとつの文字では中国語などの他のコードと 区別できない可能性があるためです。