在這一章,咱們將創建一個垃圾郵件過濾分類模型。咱們將使用一個包含垃圾郵件和非垃圾郵件的原始電子郵件數據集,並使用它來訓練咱們的ML模型。咱們將開始遵循上一章討論的開發ML模型的步驟。這將幫助咱們理解工做流程。正則表達式
在本章中,咱們將討論如下主題:算法
l 定義問題編程
l 準備數據c#
l 數據分析數組
l 構建數據的特徵框架
l 邏輯迴歸與樸素貝葉斯的Email垃圾郵件過濾編輯器
l 驗證分類模型ide
讓咱們從定義本章要解決的問題開始。咱們可能已經對垃圾郵件很熟悉了;垃圾郵件過濾是衆電子郵件服務的基本功能。垃圾郵件對用戶來講多是惱人的,但它們除此以外,也會帶來更多的問題和風險。例如,能夠設計垃圾郵件來獲取信用卡號或銀行賬戶信息,這些信息可用於信用卡欺詐或洗錢。垃圾郵件也能夠用來獲取我的數據,而後能夠用於身份盜竊和各類其餘犯罪。垃圾郵件過濾技術是電子郵件服務避免用戶遭受此類犯罪的重要一步。然而,有正確的垃圾郵件過濾解決方案是困難的。咱們想過濾掉可疑的郵件,但同時,咱們又不想過濾太多,以致於非垃圾郵件進入垃圾郵件文件夾,永遠不會被用戶看到。爲了解決這個問題,咱們將讓咱們的ML模型從原始電子郵件數據集中學習,並使用主題行將可疑電子郵件歸類爲垃圾郵件。咱們將着眼於兩個性能指標來衡量咱們的成功:準確度和召回率。咱們將在如下幾節中詳細討論這些指標。函數
總結咱們的問題定義:工具
n 須要解決的問題時什麼?咱們須要一個垃圾郵件過濾解決方案,以防止咱們的用戶成爲欺詐活動的受害者,同時改善用戶體驗。
n 爲何這是個問題?在過濾可疑郵件和不過濾太多郵件之間取得適當的平衡是很困難的,這樣垃圾郵件仍然會進入收件箱。咱們將依靠ML模型來學習如何對這些可疑郵件進行統計分類。
n 解決這個問題的方法有哪些?咱們將創建一個分類模型,根據郵件的主題行,標記潛在的垃圾郵件。咱們將使用準確性和召回率來平衡被過濾的郵件數量。
n 成功的標準是什麼?咱們想要高回覆率(實際垃圾郵件檢索的百分比佔垃圾郵件的總數),而不犧牲太多的精確率(正確分類的垃圾郵件的百分比中預測爲垃圾郵件)。
如今,咱們已經清楚地描述和定義了將要用ML解決的問題,接下來咱們須要準備數據。一般,咱們須要在數據準備步驟以前採起額外的步驟來收集咱們須要的數據,可是如今,咱們將使用一個預先編譯並標記爲公共可用的數據集。在本章中,咱們將使用CSDMC2010垃圾數據集來訓練和測試咱們的模型。咱們將看到一個名爲SPAMTrain.label的文本文件。SPAMTrain.label文件對訓練文件夾中的每封郵件都進行了編碼,0表明垃圾郵件,1表明非垃圾郵件。咱們將使用此文本文件和訓練文件夾中的電子郵件數據來構建垃圾郵件分類模型。
咱們如今擁有的是一個原始數據集,其中包含許多EML文件,其中包含關於單個電子郵件的信息,以及一個包含標記信息的文本文件。爲了使這個原始數據集可用來構建垃圾郵件分類模型,咱們須要作如下工做:
這個準備數據步驟的代碼以下:
1 using Deedle; 2 using EAGetMail; 3 using System; 4 using System.IO; 5 using System.Linq; 6 7 namespace 準備數據 8 { 9 internal class Program 10 { 11 private static void Main(string[] args) 12 { 13 // 獲取全部原始的電子郵件格式的文件 14 // TODO: 更改指向數據目錄的路徑 15 string rawDataDirPath = @"D:\工做\代碼庫\AI\垃圾郵件過濾\raw-data"; 16 string[] emailFiles = Directory.GetFiles(rawDataDirPath, "*.eml"); 17 18 // 從電子郵件文件中解析出主題和正文 19 var emailDF = ParseEmails(emailFiles); 20 // 獲取每一個電子郵件的標籤(spam vs. ham) 21 var labelDF = Frame.ReadCsv(rawDataDirPath + "\\SPAMTrain.label", hasHeaders: false, separators: " ", schema: "int,string"); 22 // 將這些標籤添加到電子郵件數據框架中 23 emailDF.AddColumn("is_ham", labelDF.GetColumnAt<String>(0)); 24 // 將解析後的電子郵件和標籤保存爲CSV文件 25 emailDF.SaveCsv("transformed.csv"); 26 27 Console.WriteLine("準備數據步驟完成!"); 28 Console.ReadKey(); 29 } 30 31 private static Frame<int, string> ParseEmails(string[] files) 32 { 33 // 咱們將解析每一個電子郵件的主題和正文,並將每一個記錄存儲到鍵值對中 34 var rows = files.AsEnumerable().Select((x, i) => 35 { 36 // 將每一個電子郵件文件加載到郵件對象中 37 Mail email = new Mail("TryIt"); 38 email.Load(x, false); 39 40 // 提取主題和正文 41 string EATrialVersionRemark = "(Trial Version)"; // EAGetMail在試用版本中附加主題「(試用版本)」 42 string emailSubject = email.Subject.EndsWith(EATrialVersionRemark) ? 43 email.Subject.Substring(0, email.Subject.Length - EATrialVersionRemark.Length) : email.Subject; 44 string textBody = email.TextBody; 45 46 // 使用電子郵件id (emailNum)、主題和正文建立鍵-值對 47 return new { emailNum = i, subject = emailSubject, body = textBody }; 48 }); 49 50 // 根據上面建立的行建立一個數據幀 51 return Frame.FromRecords(rows); 52 } 53 } 54 }
運行這段代碼後,程序將會建立一個名爲transformed.csv的文件,它將包含四列(emailNum、subject、body和is_ham)。咱們將使用此輸出數據做爲後面步驟的輸入,以構建垃圾郵件過濾項目的ML模型。可是,咱們也能夠嘗試使用Deedle框架和EAGetMail包,以不一樣的方式調整和準備這些數據。我在這裏提供的代碼是準備這些原始電子郵件數據以供未來使用的一種方法,以及咱們能夠從原始電子郵件數據中提取的一些信息。使用EAGetMail包,咱們也能夠提取其餘特徵,好比發件人的電子郵件地址和電子郵件中的附件,這些額外的特徵可能有助於改進垃圾郵件分類模型。
在準備數據步驟中,咱們將原始數據集轉換爲更具可讀性和可用性的數據集。咱們如今有一個文件能夠查看,以找出哪些郵件是垃圾郵件,哪些不是。此外,咱們能夠很容易地找到垃圾郵件和非垃圾郵件的主題行。有了這些轉換後的數據,讓咱們開始看看數據其實是什麼樣子的,看看咱們可否在數據中找到任何模式或問題。
由於咱們正在處理文本數據,因此咱們首先要看的是垃圾郵件和非垃圾郵件的單詞分佈有什麼不一樣。爲此,咱們須要將上一步的數據輸出轉換爲單詞出現次數的矩陣表示。讓咱們以數據中的前三個主題行爲例,一步步地完成這一工做。咱們的前三個主題以下:
若是咱們轉換這些數據,使每一列對應於每個主題行中的每一個單詞,並將每一個單元格的值編碼爲1,若是給定的主題行有單詞,則編碼爲0,若是沒有,則生成的矩陣以下所示:
這種特定的編碼方式稱爲one-hot編碼,咱們只關心特定的單詞是否出如今主題行中,而不關心每一個單詞在主題行中實際出現的次數。在前面的例子中,咱們還去掉了全部的標點符號,好比冒號、問號和感嘆號。要以編程方式作到這一點,咱們可使用regex將每一個主題行拆分爲只包含字母-數字字符的單詞,而後用one-hot編碼構建一個數據框架。完成這個編碼步驟的代碼以下:
1 private static Frame<int, string> CreateWordVec(Series<int, string> rows) 2 { 3 var wordsByRows = rows.GetAllValues().Select((x, i) => 4 { 5 var sb = new SeriesBuilder<string, int>(); 6 7 ISet<string> words = new HashSet<string>( 8 Regex.Matches( 9 // 隻字母字符 10 x.Value, "[a-zA-Z]+('(s|d|t|ve|m))?" 11 ).Cast<Match>().Select( 12 // 而後,將每一個單詞轉換爲小寫字母 13 y => y.Value.ToLower() 14 ).ToArray() 15 ); 16 17 // 對每行出現的單詞進行1的編碼 18 foreach (string w in words) 19 { 20 sb.Add(w, 1); 21 } 22 23 return KeyValue.Create(i, sb.Series); 24 }); 25 26 // 從咱們剛剛建立的行建立一個數據框架 並將缺失的值編碼爲0 27 var wordVecDF = Frame.FromRows(wordsByRows).FillMissing(0); 28 29 return wordVecDF; 30 }
有了這種one-hot編碼矩陣表示的單詞,使咱們的數據分析過程變的更容易。例如,若是咱們想查看垃圾郵件中出現頻率最高的10個單詞,咱們能夠簡單地對垃圾郵件的一個one-hot編碼單詞矩陣的每一列的值進行求和,而後取求和值最高的10個單詞。這正是咱們在如下代碼中所作的:
1 var hamTermFrequencies = subjectWordVecDF.Where( 2 x => x.Value.GetAs<int>("is_ham") == 1 3 ).Sum().Sort().Reversed.Where(x => x.Key != "is_ham"); 4 5 var spamTermFrequencies = subjectWordVecDF.Where( 6 x => x.Value.GetAs<int>("is_ham") == 0 7 ).Sum().Sort().Reversed; 8 9 // 查看排名前十的垃圾郵件和非垃圾郵件 10 var topN = 10; 11 12 var hamTermProportions = hamTermFrequencies / hamEmailCount; 13 var topHamTerms = hamTermProportions.Keys.Take(topN); 14 var topHamTermsProportions = hamTermProportions.Values.Take(topN); 15 16 System.IO.File.WriteAllLines( 17 dataDirPath + "\\ham-frequencies.csv", 18 hamTermFrequencies.Keys.Zip( 19 hamTermFrequencies.Values, (a, b) => string.Format("{0},{1}", a, b) 20 ) 21 ); 22 23 var spamTermProportions = spamTermFrequencies / spamEmailCount; 24 var topSpamTerms = spamTermProportions.Keys.Take(topN); 25 var topSpamTermsProportions = spamTermProportions.Values.Take(topN); 26 27 System.IO.File.WriteAllLines( 28 dataDirPath + "\\spam-frequencies.csv", 29 spamTermFrequencies.Keys.Zip( 30 spamTermFrequencies.Values, (a, b) => string.Format("{0},{1}", a, b) 31 ) 32 );
從這段代碼能夠看出,咱們使用Deedle的數據框架的求和方法來對每一列中的值求和,並按相反的順序排序。咱們對垃圾郵件這樣作一次,對非垃圾郵件這樣作一次。而後,咱們使用Take方法得到垃圾郵件和非垃圾郵件中出現頻率最高的十個單詞。當問運行這段代碼時,它將生成兩個CSV文件:ham-frequency-cies.csv和spam-frequency-cies.csv。這兩個文件包含關於垃圾郵件和非垃圾郵件中出現的單詞數量的信息,咱們將在稍後的構造數據特徵和模型構建步驟中使用這些信息。
如今讓咱們將一些數據可視化,以便進一步分析。首先,看一下數據集中ham電子郵件中出現頻率最高的10個術語:
從這個柱狀圖中能夠看出,數據集中的非垃圾郵件比垃圾郵件要多,就像在現實世界中同樣。咱們的收件箱裏收到的非垃圾郵件比垃圾郵件要多。
咱們使用如下代碼來生成這個柱狀圖,以可視化數據集中的ham和spam電子郵件的分佈:
1 var barChart = DataBarBox.Show( 2 new string[] { "Ham", "Spam" }, 3 new double[] { 4 hamEmailCount, 5 spamEmailCount 6 } 7 ); 8 barChart.SetTitle("Ham vs. Spam in Sample Set");
使用Accord.Net中的DataBarBox類。咱們能夠很容易地在柱狀圖中可視化數據。如今讓咱們來看看在ham和spam郵件中出現頻率最高的十個詞。可使用下面的代碼來爲ham和spam郵件中排名前十的術語生成柱狀圖:
1 var hamBarChart = DataBarBox.Show( 2 topHamTerms.ToArray(), 3 new double[][] { 4 topHamTermsProportions.ToArray(), 5 spamTermProportions.GetItems(topHamTerms).Values.ToArray() 6 } 7 ); 8 hamBarChart.SetTitle("Top 10 Terms in Ham Emails (blue: HAM, red: SPAM)"); 9 System.Threading.Thread.Sleep(3000); 10 hamBarChart.Invoke( 11 new Action(() => 12 { 13 hamBarChart.Size = new System.Drawing.Size(5000, 1500); 14 }) 15 ); 16 17 var spamBarChart = DataBarBox.Show( 18 topSpamTerms.ToArray(), 19 new double[][] { 20 hamTermProportions.GetItems(topSpamTerms).Values.ToArray(), 21 topSpamTermsProportions.ToArray() 22 } 23 ); 24 spamBarChart.SetTitle("Top 10 Terms in Spam Emails (blue: HAM, red: SPAM)");
相似地,咱們使用DataBarBox類來顯示條形圖。當運行這段代碼時,咱們將看到下面的圖,其中顯示了在ham電子郵件中出現頻率最高的10個術語:
spam郵件中最常出現的十大術語的柱狀圖以下:
正如所料,垃圾郵件中的單詞分佈與非垃圾郵件有很大的不一樣。例如,若是你看一下上上邊的圖表,spam和hibody這兩個詞在垃圾郵件中出現的頻率很高,但在非垃圾郵件中出現的頻率不高。然而,有些事情並無多大意義。若是你仔細觀察,你會發現全部的垃圾郵件和非垃圾郵件都有trial和version這兩個單詞,是不太可能的。若是你在文本編輯器中打開一些原始的EML文件,你會很容易發現並非全部的電子郵件的標題行都包含這兩個詞。
那麼,到底發生了什麼?咱們的數據是否被以前的數據準備或數據分析步驟污染了?
進一步的研究代表,咱們使用的其中一個軟件包致使了這個問題。咱們用來加載和提取電子郵件內容的EAGetMail包在使用其試用版本時,會自動將(Trial Version)附加到主題行末尾。如今咱們知道了這個數據問題的根本緣由,咱們須要回去修復它。一種解決方案是返回到數據準備步驟,用如下代碼更新ParseEmails函數,它只是從主題行刪除附加的(Trial Version)標誌:
1 private static Frame<int, string> ParseEmails(string[] files) 2 { 3 // 咱們將解析每一個電子郵件的主題和正文,並將每一個記錄存儲到鍵值對中 4 var rows = files.AsEnumerable().Select((x, i) => 5 { 6 // 將每一個電子郵件文件加載到郵件對象中 7 Mail email = new Mail("TryIt"); 8 email.Load(x, false); 9 10 // 提取主題和正文 11 string EATrialVersionRemark = "(Trial Version)"; // EAGetMail在試用版本中附加主題「(試用版本)」 12 string emailSubject = email.Subject.EndsWith(EATrialVersionRemark) ? 13 email.Subject.Substring(0, email.Subject.Length - EATrialVersionRemark.Length) : email.Subject; 14 string textBody = email.TextBody; 15 16 // 使用電子郵件id (emailNum)、主題和正文建立鍵-值對 17 return new { emailNum = i, subject = emailSubject, body = textBody }; 18 }); 19 20 // 根據上面建立的行建立一個數據幀 21 return Frame.FromRecords(rows); 22 }
在更新了這段代碼並再次運行以前的數據準備和分析代碼以後,word分佈的柱狀圖就更有意義了。
下面的條形圖顯示了修復和刪除(Trial Version)標記後,ham郵件中出現頻率最高的10個術語:
下面的條形圖顯示了修復和刪除(Trial Version)標誌後spam郵件中出現頻率最高的10個術語
這是一個很好的例子,說明了在構建ML模型時數據分析步驟的重要性。在數據準備和數據分析步驟之間進行迭代是很是常見的,由於咱們一般會在分析步驟中發現數據的問題,一般咱們能夠經過更新數據準備步驟中使用的一些代碼來提升數據質量。如今,咱們已經有了主題行中使用的單詞的矩陣表示形式的清晰數據,是時候開始研究咱們將用於構建ML模型的實際特性了。
在前面的步驟中,咱們簡要地查看了垃圾郵件和非垃圾郵件的單詞分類,咱們注意到了一些事情。首先,大量的最頻繁出現的單詞是常用的單詞,沒有什麼意義。例如,像to、the、For和a這樣的單詞是經常使用的單詞,而咱們的ML算法不會從這些單詞中學到什麼。這些類型的單詞被稱爲中止單詞,它們常常被忽略或從功能集中刪除。咱們將使用NLTK的中止單詞列表從功能集中過濾出經常使用的單詞。
過濾這些中止字的一種方法是以下代碼所示:
1 //讀停詞表 2 ISet<string> stopWords = new HashSet<string>(File.ReadLines(<path-to-your-stopwords.txt>); 3 //從詞頻序列中過濾出中止詞 4 var spamTermFrequenciesAfterStopWords = spamTermFrequencies.Where( 5 x => !stopWords.Contains(x.Key) 6 );
通過濾後,非垃圾郵件常出現的十大新詞語以下:
過濾掉中止詞後,垃圾郵件最常出現的十大詞語以下:
從這些柱狀圖中能夠看出,過濾掉特性集中的中止詞,使得更有意義的詞出如今頻繁出現的單詞列表的頂部。然而,咱們還注意到一件事。數字彷佛是最常出現的單詞之一。例如,數字3和2進入了非垃圾郵件中出現頻率最高的10個單詞。數字80和70進入了垃圾郵件中出現頻率最高的10個單詞。然而,很難肯定這些數字是否有助於訓練ML模型將電子郵件歸類爲垃圾郵件或垃圾郵件。
有多種方法能夠從特性集中過濾掉這些數字,可是咱們將只在這裏展現一種方法。咱們更新了上一步中使用的正則表達式,以匹配只包含字母字符而不包含字母數字字符的單詞。下面的代碼展現了咱們如何更新CreateWordVec函數來過濾掉特性集中的數字。
1 private static Frame<int, string> CreateWordVec(Series<int, string> rows) 2 { 3 var wordsByRows = rows.GetAllValues() 4 .Select((x, i) => 5 { 6 var sb = new SeriesBuilder<string, int>(); 7 ISet<string> words = new HashSet<string>( 8 //僅字母字符 9 Regex.Matches(x.Value, "[a-zA-Z]+('(s|d|t|ve|m))?") 10 .Cast<Match>() 11 //而後,將每一個單詞轉換爲小寫字母 12 .Select(y => y.Value.ToLower()) 13 .ToArray() 14 ); 15 //對每行出現的單詞進行1的編碼 16 foreach (string w in words) 17 { 18 sb.Add(w, 1); 19 } 20 return KeyValue.Create(i, sb.Series); 21 }); 22 //從咱們剛剛建立的行中建立一個數據幀,並用0對缺失的值進行編碼 23 var wordVecDF = Frame.FromRows(wordsByRows).FillMissing(0); 24 return wordVecDF; 25 }
一旦咱們從功能集過濾掉這些數字,非垃圾郵件的單詞分佈以下:
而垃圾郵件的單詞分佈,在過濾掉來自功能集的數字後,看起來像這樣:
能夠從這些柱狀圖中看到,咱們有更多的有意義的詞在頂部的名單上,這彷佛和以前有一個很大的區別,在垃圾郵件和非垃圾郵件的單詞分佈。那些常常出如今垃圾郵件中的單詞在非垃圾郵件中彷佛並很少見,反之亦然。
一旦您運行這段代碼時,它將生成柱狀圖顯示垃圾郵件單詞分佈和非垃圾郵件和兩個單詞列表的CSV files-one非垃圾郵件與相應項出現和另外一個電子郵件在垃圾郵件單詞列表和相應的項出現。在下面的模型構建部分中,當咱們爲垃圾郵件過濾構建分類模型時,咱們將使用這個術語頻率輸出來進行特徵選擇過程。
咱們已經走了很長的路,最終在c#中構建了咱們的第一個ML模型。在本節中,咱們將訓練邏輯迴歸和樸素貝葉斯分類器來將電子郵件分爲垃圾郵件和非垃圾郵件。咱們將使用這兩種學習算法來進行交叉驗證,以更好地瞭解咱們的分類模型在實踐中的表現。如前一章所簡要討論的,在k-fold交叉驗證中,訓練集被劃分爲k個大小相等的子集,其中一個子集做爲驗證集,其他的k-1子集用於訓練模型。而後重複這個過程k次,在每次迭代中使用不一樣的子集或摺疊做爲測試的驗證集,而後對相應的k驗證結果求平均值以報告單個估計。
讓咱們首先看看如何使用Accord在c#中用邏輯迴歸來實例化交叉驗證算法。代碼以下:
1 var cvLogisticRegressionClassifier = CrossValidation.Create<LogisticRegression, 2 IterativeReweightedLeastSquares<LogisticRegression>, double[], int>( 3 // 摺疊數量 4 k: numFolds, 5 // 學習算法 6 learner: (p) => new IterativeReweightedLeastSquares<LogisticRegression>() 7 { 8 MaxIterations = 100, 9 Regularization = 1e-6 10 }, 11 // 使用0 - 1損失函數做爲成本函數 12 loss: (actual, expected, p) => new ZeroOneLoss(expected).Loss(actual), 13 // 合適的分類器 14 fit: (teacher, x, y, w) => teacher.Learn(x, y, w), 15 // 輸入 16 x: input, 17 // 輸出 18 y: output 19 ); 20 // 運行交叉驗證 21 var result = cvLogisticRegressionClassifier.Learn(input, output);
讓咱們更深刻地看看這段代碼。經過提供要訓練的模型類型、適合模型的學習算法類型、輸入數據類型和輸出數據類型,咱們可使用靜態create函數建立一個新的交叉驗證算法。對於這個例子,咱們建立了一個新的交叉驗證算法,以邏輯迴歸爲模型,以IterativeReweightedLeastSquares做爲學習算法,以雙數組做爲輸入類型,以整數做爲輸出類型(每一個標籤)。您能夠嘗試使用不一樣的學習算法來訓練邏輯迴歸模型。在協議。您能夠選擇使用隨機梯度降低算法(LogisticGradientDescent)做爲適合邏輯迴歸模型的學習算法。
對於參數,咱們能夠爲k-fold交叉驗證(k)、帶有自定義參數的學習方法(learner)、選擇的損失/成本函數(loss)和一個知道如何使用學習算法(fit)來擬合模型的函數(x)、輸入(x)和輸出(y)指定摺疊數。爲了在本節中進行說明,咱們爲k-fold交叉驗證設置了一個相對較小的數字3。此外,對於最大的迭代,咱們選擇了一個相對較小的數字,100,而對於迭代加權最小二乘學習算法的正則化,咱們選擇了一個相對較大的數字,le-6或1/1,000,000。對於損耗函數,咱們使用一個簡單的0 - 1損耗函數,它爲正確的預測分配0,爲錯誤的預測分配1。這就是咱們的學習算法試圖最小化的代價函數。全部這些參數均可以進行不一樣的調優。咱們能夠選擇一個不一樣的損耗/成本函數,k摺疊交叉驗證中使用的摺疊數,以及學習算法的最大迭代次數和正則化次數。咱們甚至可使用不一樣的學習算法來適應邏輯迴歸模型,好比LogisticGradientDescent,它將迭代地嘗試找到損失函數的局部最小值。
咱們能夠用一樣的方法訓練樸素貝葉斯分類器,用k次交叉驗證。使用樸素貝葉斯學習算法進行k-fold交叉驗證的代碼以下:
1 var cvNaiveBayesClassifier = CrossValidation.Create<NaiveBayes<BernoulliDistribution>, 2 NaiveBayesLearning<BernoulliDistribution>, double[], int>( 3 // 摺疊的數量 4 k: numFolds, 5 // 二項分佈的樸素貝葉斯分類器 6 learner: (p) => new NaiveBayesLearning<BernoulliDistribution>(), 7 // 使用0 - 1損失函數做爲成本函數 8 loss: (actual, expected, p) => new ZeroOneLoss(expected).Loss(actual), 9 // 合適的分類器 10 fit: (teacher, x, y, w) => teacher.Learn(x, y, w), 11 // 輸入 12 x: input, 13 // 輸出 14 y: output 15 ); 16 // 運行交叉驗證 17 var result = cvNaiveBayesClassifier.Learn(input, output);
以前的邏輯迴歸模型代碼與這段代碼的惟一區別是咱們選擇的模型和學習算法。咱們使用NaiveBayes做爲模型,NaiveBayesLearning做爲學習算法來訓練咱們的NaiveBayes分類器,而不是使用LogisticRegression和IterativeReweightedLeastSquares。因爲全部的輸入值都是二進制的(0或1),因此咱們使用BernoulliDistribution做爲咱們的樸素Byes分類器模型。
當你運行這段代碼,你應該看到一個輸出以下:
在下面討論模型驗證方法的小節中,咱們將進一步研究這些數字所表明的內容。爲了嘗試不一樣的ML模型。可使用咱們前面討論過的邏輯迴歸模型代碼來替換它們,或者也能夠嘗試選擇不一樣的學習算法使用。
咱們使用Accord.Net Framework在c#中創建了第一個ML模型。然而,咱們尚未徹底完成。若是咱們更仔細地查看之前的控制檯輸出,就會發現有一件事很是使人擔心的情形。訓練偏差約爲0.03,而驗證偏差約爲0.26。這意味着咱們的分類模型在訓練集中正確預測了100次中的87次,而在驗證或測試集中正確預測了100次中的74次。這是一個典型的過分擬合的例子,其中模型與訓練集很是接近,以致於它對未預見數據集的預測是不可靠和不可預測的。若是咱們將這個模型應用到垃圾郵件過濾系統中,那麼實際用於過濾垃圾郵件的模型性能將是不可靠的,而且會與咱們在訓練集中看到的有所不一樣。
過分擬合一般是由於模型對於給定的數據集來講太複雜,或者使用了太多的參數來擬合模型。咱們在上一節中創建的樸素貝葉斯分類器模型的過擬合問題極可能是因爲咱們用來訓練模型的複雜性和特徵的數量。
若是再次查看上一節末尾的控制檯輸出,咱們能夠看到用於訓練樸素貝葉斯模型的特性的數量是2,212。這太多了,考慮到咱們只有約4200封電子郵件記錄,在咱們的樣本集只有三分之二(或大約3000條記錄)被用來訓練咱們的模型(這是由於咱們使用三倍交叉驗證,只有兩三個摺疊用做訓練集在每一個迭代)。爲了解決這個過擬合問題,咱們必須減小用於訓練模型的特性的數量。爲了作到這一點,咱們能夠過濾掉那些不常常出現的項。完成此任務的代碼,以下所示:
1 // 改變特徵的數量以減小過分擬合 2 int minNumOccurences = 1; 3 string[] wordFeatures = indexedSpamTermFrequencyDF.Where( 4 x => x.Value.GetAs<int>("num_occurences") >= minNumOccurences 5 ).RowKeys.ToArray(); 6 Console.WriteLine("Num特徵選擇: {0}", wordFeatures.Count());
從這段代碼能夠看出,咱們在前一節中構建的Naive Bayes分類器模型至少使用了垃圾郵件中出現的全部單詞。
若是咱們查看垃圾郵件中的單詞頻率,大約有1400個單詞只出現一次(查看在數據分析步驟中建立的spam-frequencies.csv文件)。直觀地說,那些出現次數少的單詞只會產生噪音,對咱們的模型來講沒有多少信息能夠學習。這告訴咱們,當咱們在前一節中最初構建分類模型時,咱們的模型將暴露在多少噪聲中。
如今咱們知道了這個過分擬合問題的緣由,讓咱們來修復它。讓咱們用不一樣的閾值來選擇特徵。咱們已經嘗試了五、十、1五、20和25,以使垃圾郵件中出現的次數最少(也就是說,咱們將minNumOccurrences設置爲五、十、15等等),並使用這些閾值訓練Naive Bayes分類器。
首先,樸素貝葉斯分類器的結果至少出現5次,以下圖所示:
首先,樸素貝葉斯分類器的結果至少出現10次,以下圖所示:
首先,樸素貝葉斯分類器的結果至少出現15次,以下圖所示:
首先,樸素貝葉斯分類器的結果至少出現20次,以下圖所示:
從這些實驗結果能夠看到,當咱們增長了最小數量的單詞出現次數和減小相應的特性數量用來訓練模型, 訓練偏差與驗證偏差之間的差距減少,訓練偏差開始與驗證偏差近似。當咱們解決過擬合問題時,咱們能夠更加確信模型將如何處理未預見的數據和生產系統。
如今咱們已經介紹瞭如何處理過擬合問題,咱們但願看看更多的模型性能度量工具:
Confusion matrix(混淆矩陣): 混淆矩陣是一個表,它告訴咱們預測模型的總體性能。每一列表示每一個實際類,每一行表示每一個預測類。對於二元分類問題,混淆矩陣是一個2×2的矩陣,其中第一行表示消極預測,第二行表示積極預測。第一列表示實際的否認,第二列表示實際的確定。下表說明了一個二元分類問題的混淆矩陣中的每一個單元格表明什麼。
True Negative (TN) :
TP、True Positive 真陽性:預測爲正,實際也爲正
FP、False Positive 假陽性:預測爲正,實際爲負
FN、False Negative 假陰性:預測與負、實際爲正
TN、True Negative 真陰性:預測爲負、實際也爲負。
從表中能夠看出,混淆矩陣描述了整個模型的性能。在咱們的例子中,若是咱們看最後一個控制檯輸出在前面的屏幕截圖,顯示了控制檯輸出的邏輯迴歸分類模型中,咱們能夠看到,TNs的數量是2847,fn的數量是606,FPs的數量是102,和76 tps的數量是772。根據這些信息,咱們能夠進一步計算真實陽性率(TPR)、真實負性率(TNR)、假陽性率(FPR)和假陰性率(FNR),以下:
使用前面的例子,咱們例子中的真實陽性率是0.56,TNR是0.97,FPR是0.03,FNR是0.44
Accuracy(準確性):準確性是正確預測的比例。使用與前面示例混淆矩陣相同的表示法,計算精度以下:
準確性是一個常用的模型性能指標,但有時它並不能很好地表明整個模型的性能。例如,若是樣本集很大程度上是不平衡的,而且,假設在咱們的樣本集中有5封垃圾郵件和95條火腿,那麼一個簡單的分類器將每封郵件都歸類爲火腿,那麼它必須有95%的準確率。然而,它永遠不會捕捉垃圾郵件。這就是爲何咱們須要查看混亂矩陣和其餘性能指標,如精度和正確率
Precision rate(精度):精度是正確的正面預測數量佔所有正面預測數量的比例。使用與以前相同的符號,咱們能夠計算出精度率以下:
若是看看過去的控制檯輸出以前的截圖的邏輯迴歸分類模型結果,精確率計算的數量除以TPs混淆矩陣,772年,由TPs的總和,FPs, 102年,772年從混淆矩陣,結果是0.88。
Recall rate(召回率):正確率是正確正面預測的數量佔實際陽性總數的比例。這是告訴咱們有多少實際的積極案例是由這個模型檢索到的一種方式。使用與前面相同的符號,咱們能夠計算召回率,以下所示:
若是看看過去的控制檯輸出在前面的截圖爲咱們的邏輯迴歸分類模式的結果,正確率計算的數量除以TPs混淆矩陣,經過TPs的總和,772年,772年和fn, 606年,混淆矩陣,其結果是0.56。
有了這些性能指標,咱們就能夠選擇最佳模型。在精度和正確率之間老是存在權衡。與其餘模型相比,具備較高準確率的模型召回率較低。對於咱們的垃圾郵件過濾問題,若是認爲正確地過濾垃圾郵件更重要,而且能夠犧牲一些經過用戶收件箱的垃圾郵件,那麼咱們能夠優化精度。另外一方面,若是認爲過濾掉儘量多的垃圾郵件更重要,即便咱們可能會過濾掉一些非垃圾郵件,那麼能夠優化正確率。選擇正確的模型不是一個簡單的決定,仔細考慮需求和成功標準是作出正確選擇的關鍵。
總之,下面是咱們能夠用來從交叉驗證結果和混淆矩陣中計算性能指標的代碼:
1 // 運行交叉驗證 2 var result = cvNaiveBayesClassifier.Learn(input, output); 3 // 訓練錯誤 4 double trainingError = result.Training.Mean; 5 //驗證錯誤 6 double validationError = result.Validation.Mean; 7 混淆矩陣:真陽性與假陽性和真陰性與假陰性: 8 // 混淆矩陣 9 GeneralConfusionMatrix gcm = result.ToConfusionMatrix(input, output); 10 float truePositive = (float)gcm.Matrix[1, 1]; 11 float trueNegative = (float)gcm.Matrix[0, 0]; 12 float falsePositive = (float)gcm.Matrix[1, 0]; 13 float falseNegative = (float)gcm.Matrix[0, 1];
訓練與驗證(測試)錯誤:用於識別過擬合問題:
1 // 計算的準確率, 精度, 召回 2 float accuracy = (truePositive + trueNegative) / numberOfSamples; 3 float precision = truePositive / (truePositive + falsePositive); 4 float recall = truePositive / (truePositive + falseNegative);
在本章中,咱們用c#構建了第一個能夠用於垃圾郵件過濾的ML模型。咱們首先定義並清楚地說明咱們要解決的問題以及成功的標準。而後,咱們從原始郵件數據中提取相關信息,並將其轉換爲一種格式,用於數據分析、特徵工程和ML模型構建步驟。
在數據分析步驟中,咱們學習瞭如何應用單一熱編碼並構建主題行中使用的單詞的矩陣表示。
咱們還從數據分析過程當中發現了一個數據問題,並瞭解瞭如何在數據準備和分析步驟之間來回迭代。
而後,咱們進一步改進了咱們的特性集,過濾掉中止單詞,並使用正則表達式將非字母數字或非字母單詞分隔開。
有了這個特徵集,咱們使用邏輯迴歸和樸素貝葉斯分類器算法創建了第一個分類模型,簡要介紹了過分擬合的危險,並學習瞭如何經過觀察準確性、精度和召回率來評估和比較模型性能。
最後,咱們還學習了精度和召回之間的權衡,以及如何根據這些度量和業務需求選擇模型。