在这一章,咱们将创建一个垃圾邮件过滤分类模型。咱们将使用一个包含垃圾邮件和非垃圾邮件的原始电子邮件数据集,并使用它来训练咱们的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模型构建步骤。
在数据分析步骤中,咱们学习了如何应用单一热编码并构建主题行中使用的单词的矩阵表示。
咱们还从数据分析过程当中发现了一个数据问题,并了解了如何在数据准备和分析步骤之间来回迭代。
而后,咱们进一步改进了咱们的特性集,过滤掉中止单词,并使用正则表达式将非字母数字或非字母单词分隔开。
有了这个特征集,咱们使用逻辑回归和朴素贝叶斯分类器算法创建了第一个分类模型,简要介绍了过分拟合的危险,并学习了如何经过观察准确性、精度和召回率来评估和比较模型性能。
最后,咱们还学习了精度和召回之间的权衡,以及如何根据这些度量和业务需求选择模型。