在這一章,我們將建立一個垃圾郵件過濾分類模型。我們將使用一個包含垃圾郵件和非垃圾郵件的原始電子郵件數據集,並使用它來訓練我們的ML模型。我們將開始遵循上一章討論的開發ML模型的步驟。這將幫助我們理解工作流程。 在本章中,我們將討論以下主題: l 定義問題 l 準備數據 l 數據分析 l 構建數據的特 ...
在這一章,我們將建立一個垃圾郵件過濾分類模型。我們將使用一個包含垃圾郵件和非垃圾郵件的原始電子郵件數據集,並使用它來訓練我們的ML模型。我們將開始遵循上一章討論的開發ML模型的步驟。這將幫助我們理解工作流程。
在本章中,我們將討論以下主題:
l 定義問題
l 準備數據
l 數據分析
l 構建數據的特征
l 邏輯回歸與朴素貝葉斯的Email垃圾郵件過濾
l 驗證分類模型
定義問題
讓我們從定義本章要解決的問題開始。我們可能已經對垃圾郵件很熟悉了;垃圾郵件過濾是眾電子郵件服務的基本功能。垃圾郵件對用戶來說可能是惱人的,但它們除此之外,也會帶來更多的問題和風險。例如,可以設計垃圾郵件來獲取信用卡號或銀行帳戶信息,這些信息可用於信用卡欺詐或洗錢。垃圾郵件也可以用來獲取個人數據,然後可以用於身份盜竊和各種其他犯罪。垃圾郵件過濾技術是電子郵件服務避免用戶遭受此類犯罪的重要一步。然而,有正確的垃圾郵件過濾解決方案是困難的。我們想過濾掉可疑的郵件,但同時,我們又不想過濾太多,以至於非垃圾郵件進入垃圾郵件文件夾,永遠不會被用戶看到。為瞭解決這個問題,我們將讓我們的ML模型從原始電子郵件數據集中學習,並使用主題行將可疑電子郵件歸類為垃圾郵件。我們將著眼於兩個性能指標來衡量我們的成功:準確度和召回率。我們將在以下幾節中詳細討論這些指標。
總結我們的問題定義:
n 需要解決的問題時什麼?我們需要一個垃圾郵件過濾解決方案,以防止我們的用戶成為欺詐活動的受害者,同時改善用戶體驗。
n 為什麼這是個問題?在過濾可疑郵件和不過濾太多郵件之間取得適當的平衡是很困難的,這樣垃圾郵件仍然會進入收件箱。我們將依靠ML模型來學習如何對這些可疑郵件進行統計分類。
n 解決這個問題的方法有哪些?我們將建立一個分類模型,根據郵件的主題行,標記潛在的垃圾郵件。我們將使用準確性和召回率來平衡被過濾的郵件數量。
n 成功的標準是什麼?我們想要高回覆率(實際垃圾郵件檢索的百分比占垃圾郵件的總數),而不犧牲太多的精確率(正確分類的垃圾郵件的百分比中預測為垃圾郵件)。
準備數據
現在,我們已經清楚地描述和定義了將要用ML解決的問題,接下來我們需要準備數據。通常,我們需要在數據準備步驟之前採取額外的步驟來收集我們需要的數據,但是現在,我們將使用一個預先編譯並標記為公共可用的數據集。在本章中,我們將使用CSDMC2010垃圾數據集來訓練和測試我們的模型。我們將看到一個名為SPAMTrain.label的文本文件。SPAMTrain.label文件對訓練文件夾中的每封郵件都進行了編碼,0代表垃圾郵件,1代表非垃圾郵件。我們將使用此文本文件和訓練文件夾中的電子郵件數據來構建垃圾郵件分類模型。
我們現在擁有的是一個原始數據集,其中包含許多EML文件,其中包含關於單個電子郵件的信息,以及一個包含標記信息的文本文件。為了使這個原始數據集可用來構建垃圾郵件分類模型,我們需要做以下工作:
- 從EML文件中提取主題行:為將來的任務準備數據的第一步是從各個EML文件中提取主題和正文。我們將使用一個名為EAGetMail的包來載入和提取EML文件中的信息。使用EAGetMail包,我們可以輕鬆地從EML文件中載入和提取主題和正文內容。一旦從電子郵件中提取了主題和正文,就需要將每行數據作為一行附加到Deedle數據框架中。
- 將提取的數據與標簽結合起來:在從各個EML文件中提取主題和正文內容之後,我們還需要做一件事。我們需要將經過編碼的標簽(垃圾郵件為0,而非垃圾郵件為1)映射到我們在前一步中創建的數據幀的每一行。如果我們打開垃圾郵件。標簽文件與任何文本編輯器,您可以看到編碼的標簽在第一列和相應的電子郵件文件名在第二列,由一個空格分隔。使用Deedle frame的ReadCsv函數,我們可以通過指定一個空格作為分隔符來輕鬆地將這個標簽數據載入到數據框架中。一旦我們將這個標記的數據載入到一個數據框架中,我們就可以簡單地將這個數據框架的第一列添加到前面步驟中使用Deedle框架的AddColumn函數創建的其他數據框架中。
- 將合併後的數據導出為CSV文件:現在我們已經有了一個包含電子郵件和標簽數據的數據框架,現在可以將該數據框架導出為CSV文件,以供將來使用。使用Deedle frame的SaveCsv函數,您可以輕鬆地將數據幀保存為CSV文件。
這個準備數據步驟的代碼如下:
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 }View Code
運行這段代碼後,程式將會創建一個名為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 }View Code
有了這種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 );View Code
從這段代碼可以看出,我們使用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");View Code
使用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)");View Code
類似地,我們使用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 }View Code
在更新了這段代碼並再次運行之前的數據準備和分析代碼之後,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 );View Code
經過濾後,非垃圾郵件常出現的十大新詞語如下:
過濾掉停止詞後,垃圾郵件最常出現的十大詞語如下:
從這些柱狀圖中可以看出,過濾掉特性集中的停止詞,使得更有意義的詞出現在頻繁出現的單詞列表的頂部。然而,我們還註意到一件事。數字似乎是最常出現的單詞之一。例如,數字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 }View Code
一旦我們從功能集過濾掉這些數字,非垃圾郵件的單詞分佈如下:
而垃圾郵件的單詞分佈,在過濾掉來自功能集的數字後,看起來像這樣:
可以從這些柱狀圖中看到,我們有更多的有意義的詞在頂部的名單上,這似乎和之前有一個很大的區別,在垃圾郵件和非垃圾郵件的單詞分佈。那些經常出現在垃圾郵件中的單詞在非垃圾郵件中似乎並不多見,反之亦然。
一旦您運行這段代碼時,它將生成柱狀圖顯示垃圾郵件單詞分佈和非垃圾郵件和兩個單詞列表的CSV files-one非垃圾郵件與相應項出現和另一個電子郵件在垃圾郵件單詞列表和相應的項出現。在下麵的模型構建部分中,當我們為垃圾郵件過濾構建分類模型時,我們將使用這個術語頻率輸出來進行特征選擇過程。
邏輯回歸與朴素貝葉斯的Email垃圾郵件過濾
我們已經走了很長的路,最終在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);View Code
讓我們更深入地看看這段代碼。通過提供要訓練的模型類型、適合模型的學習演算法類型、輸入數據類型和輸出數據類型,我們可以使用靜態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);View Code
之前的邏輯回歸模型代碼與這段代碼的唯一區別是我們選擇的模型和學習演算法。我們使用NaiveBayes作為模型,NaiveBayesLearning作為學習演算法來訓練我們的NaiveBayes分類器,而不是使用LogisticRegression和IterativeReweightedLeastSquares。由於所有的輸入值都是二進位的(0或1),所以我們使用BernoulliDistribution作為我們的朴素Byes分類器模型。
當你運行這段代碼,你應該看到一個輸出如下:
在下麵討論模型驗證方法的小節中,我們將進一步研究這些數字所代表的內容。為了嘗試不同的ML模型。可以使用我們前面討論過的邏輯回歸模型代碼來替換它們,或者也可以嘗試選擇不同的學習演算法使用。
驗證分類模型
我們使用Accord.Net Framework在c#中建立了第一個ML模型。然而,我們還沒有完全完成。如果我們更仔細地查看以前的控制台輸出,就會發現有一件事非常令人擔憂的情形。訓練誤差約為0.03,而驗證誤差約為0.26。這意味著我們的分類模型在訓練集中正確預測了100次中的87次,而在驗證或測試集中正確預測了100次中的74次。這是一個典型的過度擬合的例子,其中模型與訓練集非常接近,以至於它對未預見數據集的預測是不