實現簡單的垃圾郵件過濾器,來講解機器學習的概念

Patrick_顏發表於2016-07-16

這個教程簡單地介紹瞭如何使用 Python 2.x 和 Weka 進行機器學習。Weka 是一個資料處理和機器學習工具。這篇文章通過實現一個簡單的垃圾郵件過濾器來講解機器學習的概念。

這篇文章是由 Codementor 團隊根據他們一個關注自然語言處理的資料科學家 Benjamin Cohen 的一個師生互動來創作的。

什麼是機器學習?

簡單來說,機器學習是一種基本資料的學習。回到以前我們沒有大量的資料和強大的計算能力的時候,人們嘗試手寫規則來解決許多問題。例如,當你看到{{some word}},它很可能是垃圾郵件。當郵件裡出現連結,它很可能是垃圾郵件。它們確實是可用的,但是由於問題變得越來越複雜,規則組合開始變得難以掌控,包括如何記錄,傳遞以及處理它們。許多解決這樣問題的技術都屬於機器學習這個範疇內。從根本上說,你要解決如何自動地從資料的某些特徵中學習他們之間的關係。

在機器學習中,我們嘗試解決的一個重要問題叫做分類。簡單地說,它決定了把東西放進什麼類。例如,識別電子郵件是否是垃圾郵件,或者識別一張圖片是狗還是貓。甚至還可以把資料分為兩種或多種。

也就是說,我們真正需要開發的是一個能夠自動區分兩個或多個類別的系統。在這次練習中,我們要實現的是一個能識別垃圾郵件和正常郵件的系統。我們不打算手動編寫規則來完成所有的這些功能。完成這個工作或者其他任何機器學習的問題的一件必要的事情就是收集資料集。機器學習的基本流程是從這些資料集中學習規則(可以代表這些資料集的規則),然後用學習到的規則來預測新的資料。換句話說,我們不需要一個資料集作為支援,告訴機器什麼是正確的,或者我們的資料看起來像什麼,也不用為機器去開發規則。我們需要的是收集讓機器學習的材料。

值得注意的是,我們要完成的任務的資料集已經準備好了。但是一般來說,當你解決一個問題的時候,你通常還沒有資料,並且你不知道你需要什麼資料。

如果你是寫一個系統來識別垃圾郵件,那麼在這個練習中我們用的資料集正是你想要的,但事實上作為一個應用程式你可能需要找到其他的資料集來交叉驗證。電子郵件的地址是一個不穩定的的特徵,它們會經常被關閉以至於傳送人會建立許多新的郵件地址。你可以做的一件事是判斷某個域名傳送垃圾郵件的概率。例如,不會有許多人用 gmail 來傳送垃圾郵件,因為谷歌對於檢測和關閉垃圾郵件賬戶做的是非常棒的。然而,一些像 hotmail 這樣的域名可能有一個特徵:如果傳送者使用了 hotmail,它很可能就是垃圾郵件,因此你完全可以從檢測郵件地址中學習到特徵。其他的一些我們能看到的典型特徵是元資訊,例如郵件是什麼時間傳送的。在這個練習中所有這些我們可以看到的資料事實上是來自郵件,即文字內容。

練習
這個是 Github 專案的連結,你可以建立分支並繼續開發這個專案。首先確定你已經在你的機器上安裝了 Weka ——一款免費的軟體。

我們將要用的資料集大概包含了1300個電子郵件。你可以快速地瀏覽這兩個資料夾看看都有什麼郵件,它們分別是垃圾郵件和非垃圾郵件。例如:

這個是相當典型的垃圾郵件,然後下面這個

是一個正常的郵件。

總而言之,快速的瀏覽了這兩個資料夾以後,你會發現它們是相當有代表性的郵件,能看出來它是垃圾郵件或者不是。這確實很重要,因為如果我們的資料不具有代表性,機器學習到的東西將沒有任何意義。

總結一下要點,我們要嘗試判斷一個特徵,看它是否能讓我們區分資料集中的非垃圾郵件和垃圾郵件。希望將來它可以幫助我們檢測出垃圾郵件。

何時用到自然語言處理?

如果你已經注意到,來自”垃圾郵件“與“非垃圾郵件”中內容都是文字。我們作為人類可以很好的理解文字和語言,但是計算機能理解的卻非常的少。在這個練習中,我們得把英語轉換成計算機可以理解的語言。一般來說,可以轉換成數字。這就是自然語言處理的由來,我們試著讓計算機理解這些資訊的上下文要點。在機器學習的部分就是理解這些數字形成模型並且在將來應用這個模型。

什麼是特徵?

在我們這個案例中,特徵主要是我們輸入資訊的一種數字化的轉變,或者說僅僅是一個機器可以學習的數字。我們打算獲取一組數字,或者被叫做特徵。它可以像這樣代表一封郵件:

我們的數字特徵挑選得越好,模型就能更有希望地識別我們的電子郵件。

在挑選任何其他特徵之前,我們直接來看字數。features.py是完成這個功能的程式碼:

但是,為了讓Weka分析這個資料,我們需要把它轉換為.arff檔案。

feature_extract.py是一個指令碼. 現在你不用擔心這是什麼,但是如果你感興趣話這裡有程式碼:

這個指令碼基本上適用於我們在features.py中寫的所有特徵並且把這些特徵放到.arff檔案中

那麼,執行feature_extract.py之後,我們來看一下spam.arff檔案:

@Relation表示問題的名字,然後每個@ATTRIBUTE是一個特徵。我們這裡第一個特徵是字數,REAL表示它是一個實數。最後一個屬性表示不同的分類,並且在我們這個例子中,SPAM可以是True或者False。執行之後,我們的資料包含了每一個屬性的值。例如,開頭是1003的那一行表示該郵件有1003個單詞,True表示它是垃圾郵件。

這個.arff 檔案並沒有什麼內容需要必須看,我們只是把它扔進Weka中。

執行Weka並讀取spam.arff檔案

在預處理選項中,左邊那一列包含了所有的特徵。如果你點選“SPAM”,在右面一列,是否是垃圾郵件的分佈圖會顯示出來。在這1388個郵件中,501個是垃圾郵件,887個不是。

讓我們用Weka看一下numwords屬性:

weka preprocess

你會看到一個分佈圖,但是現在它並沒有告訴我們很多。我們也會看到一些統計數字。例如,最少的字數是1,最大是13894,這是非常高的。由於我們有更多的特徵,我們可以看到他們在分佈中更多的不同之處,以及告訴我們哪一個是好的。

如何選擇,創造,以及使用特徵

下一件要做的事是檢視Classify選項。

在這個選項中有許多資訊,但是在這次任務中我們暫時忽略大多數功能。一個我們將會用到的重要功能是 classifiers box,這裡面的 classifier 是我們如何學習模型的關鍵。根據你的問題,特定的分類器對特定的問題會非常的好用。如果你點選了 classifier box,你會看到在目錄中有很多可展開的檔案。

然後,我們可以選擇測試選項。我們不僅僅需要訓練模型,也需要一些方法來檢驗通過我們的特徵學習的或觀察的模型是否好用。我們可以學習一個模型,但是如果它被證明是完全沒有用的,不會對我們有什麼幫助。

在測試選項中,我們選擇用 Percentage split。

在 percentage split 後面填寫 80% 後,它會用 80% 的資料訓練一個模型。在我們這個專案中,1300 封電子郵件中大概有 1000 封左右用來訓練。剩下的 20% 將被用來測試,然後我們會看看有多少比例的資料是正確的。

我們使用 percentage split 是因為我們不想遇到一個叫做“過擬合”的問題:從我們的資料中學習到的模型可能在現實世界中不存在。如果我們學習一個非常具體的資料集,而它們只是偶然的出現在我們的訓練資料中,這個模型不會幫我們預測任何事,因為他不會在其他資料中被觀察到。我們要牢記的是使用我們從未見過的資料來測試是非常重要的,因此我們可以模擬現實世界中的資料來測試我們的模型,而不是用訓練過的資料繼續測試。

我們最後會看一個過擬合的例子。現在首先選擇一個叫做OneR的演算法測試一下字數特徵的表現如何。

我們要重視的一個數字是“correctly classified instances”:

這些數字代表我們的垃圾郵件檢測率。可以看到,使用這些特徵我們把 66.9% 的電子郵件正確的分類為垃圾郵件或者非垃圾郵件。第一次的檢驗結果看起來有不錯的效果。

然而,66.8% 的準確率並不是很好,因為如果我們檢視初始的資料分佈會看到,我們有 1388 封郵件,887 封不是垃圾郵件,或者說 64% 不是垃圾郵件,因此 67% 的檢測率並不高。

這個概念叫做基準線,它是個非常簡單的分類,我們用來跟其他的結果進行比較。如果我們資料的分佈是 50:50,並且我們得到 66.9% 的正確率的話,那麼這是一個非常棒的結果因為我們提升了大概 17% 。但是我們僅僅提升了 3% 。

什麼是混淆矩陣

在weka下面的部分,我們需要檢視的資料是混淆矩陣:

“a b” 那一行代表預測的類別,而“a,b”列代表實際的分類。這裡 a 代表 spam = True,而 b 代表 spam = False

因此,在我們這個案例中,我們正確的分類43個例項,而分錯了52個。最下面那一行展示了我們混淆的部分,或者說是我們做錯的部分。

這個混淆矩陣很有幫助,因為如果左下角是 0 的話就表明我們把所有的資料分到了是垃圾郵件一類,但是它也意味著分類過於極端,我們應該通過微調引數來使我們的規則放鬆一點。

提升Weka的分類準確率

為了提高準確率,讓我們回到 features.py 檔案,然後寫一些程式碼獲得更多的特徵。

我們當前的特徵是獲取郵件的所有文字內容並且返回字數。然後,它會通過 features_extract.py 自動的寫入到 spam.arff 檔案中。現在我們要加入一個特徵來檢測該郵件的內容包含在 HTML 中還是僅僅是純文字內容。

我選擇加入這個特徵是因為我知道這會產生一個有趣的分佈。一般來說,你可以通過觀察資料集而快速地想出好的特徵。由於你已經瀏覽了一些在非垃圾郵件資料夾中的檔案,因此可以看到只有很少的正常郵件出現了 HTML 的格式。在垃圾郵件資料夾中,一些郵件似乎也在 HTML 格式,因此它可能是一個好的特徵。

由於這次的任務只花費了不到 1 小時的時間,我們寫出的是大概的方法來檢測email是否在 HTML 中,它並不完美。

我們認為如果郵件中出現了單詞 HTML 則該郵件就有這個特徵,可能並非如此,但這是最簡單的方法。

在命令列執行 features_extract.py 指令碼檔案以後

用 Weka 重新讀取 .arff 檔案然後會看到加入了 has_html 的特徵:

在這裡平均值和標準差不會講的太多。看一下我們的 SPAM 特徵的圖表,紅色代表的是非垃圾郵件。因此我們可以看到在 has_html 這個特徵中,多數的非垃圾郵件不包含 HTML ,並且多數的垃圾郵件含有 HTML 。這正是我們期待的,至少目測來看,我們是可以從這個特徵中學習到東西的,因此這是一個很好的特徵。

讓我們繼續在 features.py 寫出第三個特徵。基於我們的資料集,我已經觀察到垃圾郵件有更多的連結由於它們想讓你買東西,因此我想取他的連結數作為特徵。

再次說明這個特徵只是個近似值,我們不會去算精確的連結數由於這是更花費時間的。這個特徵是假設任何時候只要電子郵件包含了 http 關鍵字,都會有相應的連結。

After running our features_extract.py again, we can re-open our spam.arff file on Weka:
再次執行 features_extract.py 檔案之後,可以在 Weka 中重新開啟 spam.arff 檔案:

正如我們期待的那樣,他們最小值是 0,因為有的電子郵件不包含連結;而其中一個的最大值是 68 。

有趣的是,如果你看了我們的分佈圖,你會發現在某些點(10 左右),所有超過 10 個連結的郵件都是垃圾郵件。因此這看起來確實是好的特徵。現在我們可以制定一個規則:如果連結數大於 10,那麼它肯定是垃圾郵件。還有一個基本的規則:如果一個郵件有更多的連結數,那麼它更可能是垃圾郵件。

我們在Weka中測試一下新的特徵,看看重新測試後準確率是否有提升。

它看起來並沒有什麼改變,儘管我們用了更好的規則。原因可能是我們用了錯誤的分類器。OneR分類器是僅僅使用一個特徵,並且基於那個特徵來開發規則,因此即使我們重新測試它,它也僅僅看到numwords特徵,並且基於它開發所有的規則。然而,現在我們有3個特徵,並且不確定基於字數特徵的分類是好用的。我們瞭解了has_html以及has_link是好用的並且想把他們合併到一起。下面切換到J48決策樹分類器。

什麼是決策樹?

決策是是一個簡單的概念,它與我們人類行為很相似。由於它本質上是由做決定開始,然後跟隨一個路徑。這裡我找了一個經典的例子 http://jmvidal.cse.sc.edu/talks/decisiontrees/allslides.html :

假如我們想去打網球。我們會看天氣是晴天,陰天,還是雨天。如果是陰天,我們就去。如果是雨天,我們會看是否颳風。如果風太大,我們就不會去。如果風很小,我們就去。如果是晴天,我們會看溼度。如果溼度是正常的我們就去玩,如果太高則不會去。

你會看到我們基於一個變數做單獨的決定,然後繼續沿著那顆樹做決定直到得出我們希望是正確的結論。建立這樣的決策樹有許多演算法,但是這裡我們不會討論他們。

不管怎樣,我們選擇了 J48 分類器。我們看到:

我們可以清楚的看到如何基於一顆樹來做決定的,如果 num_link 大於 3 則為 True 。如果小於 3,我們會看它是否包含 html,同時它會把樹分成兩個樹枝。然後我們繼續根據其他的特徵例如 num_link 和 numwords 向下做決策。

正如上面截圖中你看到的那樣,分類正確率已經提高到76.6%。

如何判斷哪個規則比較重要

我將會給出一個簡化的答案,由於完整的答案太過於複雜。從根本上說,有一個概念叫做”資訊“,或者叫知識。我們可以這樣問自己:”通過這個特徵我們可以獲得多少資訊呢?“。

例如,如果有兩個特徵:晴天和多雲天。我們在 4 個晴天和 4 個多雲天打網球,並且在兩個晴天和多雲天不大網球。這樣的資訊沒有跟我們任何幫助——我們通過觀察天氣沒有獲得任何資訊,因為它是雙向選擇的;

如果我們觀察多風天,每一次多風天我們不打網球,以及如果是多風天我們總是去打網球,那麼我們通過檢測多風的特徵就獲得了許多資訊。因此,相對於晴天,觀察每天是否多風會更有收益,這是因為我們通過觀察晴天特徵沒有獲取任何資訊。

觀察我們的模型你會注意到,我們首先會檢查連結個數,那就意味著連結個數是一個獲取很多資訊的特徵。事實上我們可以通過 Weka 來觀察原始資料的資訊獲取情況。

讓我們選擇進入屬性選擇選項,選擇 InfoGainAttributeEval 然後點選開始。會出現這個畫面:

我們不需要關心所有數字的意義,但是我們可以看到 num_link 這個特徵獲取了大部分的資訊,緊跟著是 has_html 和 numwords 。換句話說, num_link 是最好的特徵。

為了進一步的闡述,我們可以在 features.py 中加一個虛擬特徵:

這個特徵不管是什麼電子郵件都會返回1。如果我們執行feature_extract.py然後在weka中重新讀取spam.arff,我們應該看到虛擬特徵沒有任何資訊獲取。

這是因為每個例項都有相同的值。

總結一下,我們本質上需要的特徵是可以獲取最多資訊的特徵。

過擬合

現在我們想要更多的特徵幫我們來識別郵件。同樣,這裡有很多種方法把郵件中所有的文字從我們可以理解的狀態轉換成計算機可以理解的狀態,它是一個單個數字或者是二元特徵。

一位 Codementor 的使用者建議使用一些具體的代表垃圾郵件的單詞,例如”free“,”buy“,”join“,”start“, ”click“,”discount“。讓我們在 features.py 檔案中加入這些特徵。通過空格分開所有的單詞然後獲得他們。

讓我們給這個特徵一個截圖並在 Weka 中重新讀取 spam.arff 。

我們可以看到在這個分佈中大多數的非垃圾郵件不包含任何垃圾郵件詞彙,並且由於計數增長,柱狀條會變得越藍,這意味著如果郵件中包含越多垃圾郵件詞彙就越有可能是垃圾郵件。

我們也來看一下資訊獲取:

我們可以看到這裡有一些資訊獲取,但是並不全。

至於我們的分類器,實際上我們可以看到準確率有輕微的下降。

並不是說 spammy_words 是個不好的特徵,在我看來它是相當好的。讓我們思考一下有什麼可能願意會導致分類準確率下降。

我個人看來,準確率下降是因為過擬合。如果你檢視 Weka 中的那棵樹,你會發現我們生成了許多規則-我估計至少有50個規則,並且他們的大多數都非常的具體。

我們可能分析資料太過於具體,並且制定的規則也太具體,尤其是 spammy_words 特徵,它並沒有對資訊獲取很多貢獻。但是我們可以用分類器做實驗並且調整引數看看是否可以增加分類器準確率,讓我嘗試修復一下我們的 spammy_words 特徵。

在之前,我們認為一些單詞會表明為它是垃圾郵件。然而,根據我們的實驗來看,作為人類我們是有偏見的,我們想到的單詞可能不會出現在我們的資料集中。或者,它可能意味著單詞‘free’僅僅是一個在郵件裡普通常見的單詞。

我們打算想個辦法過濾垃圾單詞:它會自動的生成一個列表而不是我們自己去想。

我們先來看一下單詞的分佈。我們應該設定垃圾郵件詞彙和非垃圾郵件詞彙的詞典,然後像下面那樣填進去:

讓我們儲存上面的程式碼到檔案 wordcounts.py 中然後執行它。我們會看到所有的單詞構成了一個超級大的字典並且描述了單詞出現的頻率。他也得到了連結和 html 標籤,但是我們要忽略他,並且可以通過改變 print spamwords 程式碼列印出這些詞。

這裡我們根據他們出現的頻率排序。

然後我們會看到排在前面的詞彙都是常見的(例如 for, a, and, you, of, to, the)。一開始我們會認為這些詞彙不是很好,因為他們幫不上忙。我們不能找“a”這樣的單詞因為它會同時出現在垃圾郵件和非垃圾郵件中。

因此,想要真正解決統計經常出現在垃圾郵件中,而不會出現在非垃圾郵件中的詞彙,我們只用改動一點程式碼,來統計出現在非垃圾郵件中的常用詞彙:os.chdir(spam_directory) 改為  os.chdir(‘../’+not_spam)。

我們會注意到很奇怪的現象,Helvetica 是一個垃圾郵件中的常用詞彙而在非垃圾郵件中並不存在。New, money, e-mail, receive, and business 也出現在垃圾郵件中但是在非垃圾郵件中出現的卻如此之少,因此讓我們對 spammy_words 特徵做出改變。

讓我們也為非垃圾郵件詞彙創造一個特徵,可能也很有幫助。有趣的是許多電子郵件拼寫是不同的。

加入這兩個特徵以後,現在在 Weka 中顯示的準確率已經接近 79% 了。事實上我們可以看到許多有趣的事情僅僅通過檢查實際的單詞。我們用這些單詞甚至單詞的組合二元文法(一對單詞)來訓練我們的機器或者三元文法(三個單詞一起)。例如,“free”可能在垃圾郵件與非垃圾郵件中都出現過,但是“free money”僅在垃圾郵件中出現。我們可以制定一個特徵如果出現了“free money”則為垃圾郵件,而只出現“free”和“money”則不是。

其他的特徵

通過觀察我們的資料集或者實驗你可能會注意到一件事情,垃圾郵件更傾向於在郵件中“表露身份”以及全部使用大寫,然而正常人很少這樣做。讓我們根據這個觀察再嘗試加入兩個特徵。第一個是郵件文字是否全部是大寫,另一個是大寫字母所佔比率即所有的大寫字母個數除以小寫字母個數。

觀察 Weka 中的結果,我們發現 all_caps 特徵並沒有給我們很多資訊,但是 cap_ratio 特徵告訴我們許多資訊,並且它的分佈圖表明超過平均值的比率特徵幾乎都是垃圾郵件。

加入cap_ratio特徵後使我們的準確率幾乎達到了86%。

結束語

隨著我們加了更多的特徵,我們的模型訓練起來變得更慢了,因為它會花費更多的時間來學習模型。現在我們已經完成了這個練習並找到了7個特徵,這已經很不錯了。

Weka 裡的選擇屬性一欄有一個非常棒的函式,它會建議你選擇那些特徵來訓練:

正如你看到的,它推薦我們使用 cap_ratio,has_html,以及 spammy_words 3個特徵。因此如果我們在預處理欄不選擇其他的特徵,然後進行測試準確率,我們可能也會得到近似於 86% 的結果。

我鼓勵你們根據所學實現你們自己的特徵,然後看看是否提高了分類準確率。我希望這次練習對你們開始學習機器學習和自然語言處理是有幫助的。

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

實現簡單的垃圾郵件過濾器,來講解機器學習的概念

相關文章