翻譯自:A Plan for Spam。
(這篇文章描述了應用在 Arc 語言的練習作品——防垃圾線上郵件閱讀器中的垃圾過濾技術。改進後的演算法在《更好的貝葉斯過濾》中描述。)
我認為阻止垃圾郵件是可能的,並且基於內容的過濾器是一種方法。垃圾郵件製作者的阿喀琉斯之踵正是他們的資訊。他們可以繞過你設定的任何其他障礙。至少到目前為止他們已經做到了。但他們必須要遞送他們的資訊,不論這資訊是什麼。如果我們可以寫一個能識別他們資訊的軟體,那他們就沒有任何辦法避開了。
對接收者來說,垃圾郵件很容易識別。如果你僱某人來閱讀你的郵件並挑出垃圾郵件,他可能不會遇到什麼困難。那麼在不用人工智慧的情況下,我們需要做多少工作來自動化這個過程?
我認為我們可以用一個相當簡單的演算法來解決這個問題。實際上我發現你可以僅僅用單詞的垃圾概率的一個貝葉斯組合就可以對現今(2002 年)的垃圾郵件過濾得不錯。使用一個稍微調整過的(如下面描述)貝葉斯過濾器,我們現在在每 1000 封垃圾郵件中漏過濾少於 5 封,並且誤判為 0。
人們開始寫垃圾過濾器時常常並不會一開始就嘗試統計的方法。許多黑客的直覺是試著寫一個能識別垃圾郵件的獨特屬性的軟體。你看著垃圾郵件並且想,這些無恥的傢伙試著給我傳送一些由“親愛的朋友”開頭或者主題完全由大寫字母組成並且以八個感嘆號結尾的郵件。我用一行程式碼就可以過濾掉這些東西。
然後你就這麼做了,並且剛開始時它確實管用。一點簡單的規則就能過濾掉不少垃圾郵件。在我的垃圾郵件語料裡,僅僅用“click”這個單詞就能過濾出 79.7%,而且只有 1.2% 的誤報率。
在嘗試統計方法之前我用了大概六個月來寫識別獨立的垃圾郵件特徵的軟體。我發現識別剩下的那一點垃圾郵件非常困難,當把過濾器調整得更嚴格之後會產生更多的誤報。
誤報是指清白的郵件被錯誤地判斷為垃圾郵件。對大多數使用者來說,錯過正常郵件比收到垃圾郵件嚴重得多,一個過濾器產生誤判就像痤瘡藥可能致死一樣。
使用者收到的垃圾郵件越多,他就越不容易注意到垃圾郵件箱裡的無辜郵件。並且怪異的的是,你的過濾器越好,誤判的危害就越大,因為當過濾器足夠好時,使用者會更加傾向於忽略所有被過濾的郵件。
我不知道為啥我這麼長時間都避免去使用統計方法。我覺得是因為我太沉醉於親自挑出垃圾郵件的特徵,就像在跟垃圾郵件傳送者玩著什麼競技遊戲一樣。(非黑客常常並不會注意到這點,但大多數的黑客是非常爭強好勝的。)當我嘗試統計分析時,我發現這比我原先的做法聰明得多。它發現了,其實是顯而易見的,諸如“virtumundo”和“teens”這樣的詞是垃圾郵件的好指示器。但是它也發現了“per”、“FL”和“ff0000”也是好的特徵。實際上“ff0000”(html 中代表紅色)對垃圾郵件分辨程度和其他色情詞語差不多。
下面是我如何做統計過濾的一個概述。我以一個垃圾郵件語料庫和一個正常郵件語料庫開始。當時每個裡面都有大約 4000 封郵件。我掃描檢查了每個語料庫的整個文字,包括頭部和嵌入的 html 和 javascript。我目前把字母數字組成的單詞、連線號、撇號和美元符號作為詞法元素,剩下的其他符號作為詞法元素的分隔符。(這裡可能有提高的空間。)我忽略了全部由數字組成的詞法元素,並且我也忽略了 html 註釋,甚至不把其看做分隔符。
我計算了每個詞(目前忽略大小寫)在每個語料庫裡出現的次數。在這個階段我得到了兩個大雜湊表,每個語料庫一個,從單詞對映到它們出現的次數。
之後我建立了第三個雜湊表,這次從每個單詞對映到當一個郵件包含它們時為垃圾郵件的概率,計算方法如下:
(let ((g (* 2 (or (gethash word good) 0)))
(b (or (gethash word bad) 0)))
(unless ( (+ g b) 5)
(max .01
(min .99 (float (/ (min 1 (/ b nbad))
(+ (min 1 (/ g ngood))
(min 1 (/ b nbad)))))))))
複製程式碼
其中 word
是我們正計算的單詞的概率,good
和 bad
是我在第一步中建立的雜湊表,ngood
和 nbad
是正常郵件和垃圾郵件對應的個數。
我用程式碼來解釋是為了展示一些重要的細節。我想輕微地讓概率偏向於避免誤判,通過試錯我發現把 good
中的計數翻倍是個好方法。這有助於區分偶爾在正常郵件中出現而幾乎從不在垃圾郵件中出現的單詞。我只考慮出現總數超過 5 次的單詞(實際上因為翻倍,在正常郵件中出現 3 次就夠了)。然後是應該給只在一個語料庫中出現的單詞什麼概率的問題。同樣通過試錯我選擇了 0.01 和 0.99。此處應該還有很多的調整空間,不過隨著語料庫增長這種調整會自動發生。
特別善於觀察的會發現我在把每個語料庫當做一個長文字流來計算單詞出現次數時,我使用每個語料庫的郵件個數,而不是它們的總和,來作為計算垃圾郵件概率的分母。這輕微地偏向於減少誤判。
當新的郵件到達時,它會被掃描分成單詞,然後取其中最有趣的 15 個單詞——這裡有趣度由它們的垃圾郵件概率和原始 0.5 的差距衡量——來計算這個郵件是垃圾郵件的概率。如果 probs
是這 15 個單詞概率的一個列表,你可以這樣計算組合概率:
(let ((prod (apply #'* probs)))
(/ prod (+ prod (apply #'* (mapcar #'(lambda (x)
(- 1 x))
probs)))))
複製程式碼
實際應用中會有一個問題是如何給一個從未見過的單詞賦以概率,比如一個在單詞概率雜湊表中沒出現過的單詞。我發現,依舊通過試錯,0.4 是一個不錯的值。如果你從未見過一個單詞,那它很可能是無辜的;垃圾郵件的單詞一般很相似。
在結尾的附錄中有這個演算法應用於實際郵件的例子。
我把此演算法給出的概率大於 0.9 的郵件當做垃圾郵件。不過在實踐中這個閾值的具體位置似乎影響不大,因為最終沒幾個概率在範圍中間。
統計方法的一個重要的優勢是你不用再去閱讀那麼多垃圾郵件了。在過去的 6 個月裡,我逐字閱讀了上千封垃圾郵件,這確實讓人噁心。Norbert Wiener 說過如果你和奴隸競爭你就會變成一個奴隸,和垃圾郵件傳送者競爭也是如此。為了識別出獨立的垃圾郵件特點你必須設法理解垃圾郵件傳送者的想法,但坦率地說我想在垃圾郵件傳送者的想法上花的時間越少越好。
但是貝葉斯方法的真正優勢,顯而易見的,是你知道你在測量什麼。如 SpamAssassin 這種特點識別過濾器賦予郵件一個垃圾“分數”。貝葉斯方法則賦予一個實際的概率。“分數”的問題在於沒人知道它的含義。使用者不知道它的含義,更糟的是,過濾器開發者也不知道。一個包含單詞“sex”的郵件應該得到多少分?一個概率可能是錯誤的,但它的含義或者如何結合證據來計算卻沒有多大歧義。基於我的語料庫,“sex”指出包含它的郵件有 0.97 的概率為垃圾郵件,同時“sexy”有 0.99 的概率。貝葉斯法則,同樣無歧義,指出同時包含這兩個單詞的郵件,在沒有其他證據的情況下(不太可能),有 99.97% 的可能是垃圾郵件。
理想中,每個使用者的概率應該獨立計算。我有許多包含“Lisp”這個單詞的正常郵件,並且沒有一封垃圾郵件包含這個。所以像這樣的單詞確實是一個給我傳送郵件的密碼。在我之前的垃圾郵件過濾軟體中,使用者可以建立一個這樣的單詞列表,如果有郵件包含它們則就會自動通過過濾器。在我的列表中我放入瞭如“Lisp”這樣的單詞和我的郵編,這樣(可能有點像垃圾郵件的)線上訂單的收據就能通過。我曾認為我這樣很聰明,但我發現貝葉斯過濾器為我做了同樣的事,此外還發現了更多的我想不到的單詞。
我之前說的我們的過濾器在 1000 封郵件中漏過濾少於 5 封並且沒有誤判,這是指在我的語料庫上過濾我的郵件。但這些數字並沒有誤導,因為這是我所支援的方法:根據各個使用者的正常郵件和垃圾郵件的語料庫來過濾它們自己的郵件。本質上,每個使用者應該有兩個刪除按鈕,普通的刪除和作為垃圾郵件的刪除。每個被作為垃圾郵件刪除的進入垃圾郵件語料庫,而所有其他的進入正常郵件語料庫。
你可以讓使用者以一個種子過濾器開始,但最終每個使用者都會擁有自己的基於他實際收到的郵件的單詞概率。這(a)讓過濾器更有效,(b)讓每個使用者決定自己關於垃圾郵件的精確定義,並且(c)可能最好的是讓垃圾郵件傳送者很難調整郵件來通過過濾器。如果很多的過濾器智慧是在個人獨立的資料庫中,他們僅僅調整垃圾郵件來通過種子過濾器並不會保證他們能夠通過獨立使用者的多樣化的並經過更多訓練的過濾器。
基於內容的垃圾郵件過濾器通常會搭配一個白名單,這個名單包含可以不過濾而直接通過的傳送者。一種建立這種白名單的方法是維護一個使用者曾傳送過郵件的目標地址。如果一個郵件閱讀者有一個作為垃圾刪除按鈕,那你可以把使用者以普通刪除的每封郵件的地址新增到其中。
我是一個白名單的支持者,但是作為節約計算的一種方法而不是提升過濾能力。我曾經認為白名單能讓過濾更容易,因為你只用過濾那些來自不認識的人的郵件,並且第一次給你發郵件的人被按慣例地限制為他們能對你說的話。某些你已經認識的人可能會給你發關於性的郵件,但第一次給你發郵件的人一般不會這麼做。問題是,人們可以有多於一個的郵箱地址,所以一個新的發件地址並不能保證傳送者是第一次給你寫信。一個老朋友突然用一個新的郵件地址給你寫信並不常見(特別是如果他是一個黑客的話),你不能承擔因嚴格過濾未知郵件地址而帶來的誤報率。
從某種意義上說,我的過濾器確實包含了一種白名單(和黑名單),因為它們基於整個資訊,包括頭部。所以它們“知道”信任的傳送者的郵件地址,甚至是傳送給我的郵件所通過的路由。它們也知道有關垃圾郵件的這些資訊,包括伺服器地址、客戶端版本和協議。