暴力破解找到垃圾邮件
人工智能和机器学习竞赛的参赛作品。我就是这样学习/猜测如何查找垃圾邮件的。
引言
我对人工智能或机器学习一无所知。然而,当我看到与竞赛相关的数据文件时,我非常好奇如何确定测试数据中的哪些行是垃圾邮件。
我决定放弃所有关于该主题的研究,只是为了学习而暴力破解。
不到 100 行 C# 代码
我很高兴能用不到 100 行 C# 代码解决这个问题。
一个类统治一切
我将所有工作代码封装在一个名为 LanguageLearner
的小类中。
来自竞赛
我这样命名它,因为它将学习哪些词表示垃圾邮件(坏)以及哪些词表示正常邮件(好)。
如果您查看了 CP 竞赛提供的数据文件(机器学习和人工智能挑战赛[^]),您就会确切地知道这意味着什么。我还将该数据文件 (SpamDetectionData.txt) 包含在本文顶部提供的项目中。
这是我的类的所有代码。所有工作都在这个类中完成。我将在下面解释如何使用它以及它具体做了什么。
class LanguageLearner {
public HashSet<String> SpamWords {get; private set;}
public HashSet<String> HamWords {get; private set;}
private string filePath;
private String currentLine;
private System.IO.StreamReader spamDataFile;
private bool displayLines;
public List<String> AllTestData {get; private set;}
private string LastLineIndicatorText;
public int hamCounter {get; private set;}
public int spamCounter {get; private set;}
public LanguageLearner(string filePath, bool displayLines = false){
this.filePath = filePath;
this.displayLines = displayLines;
spamDataFile = new System.IO.StreamReader(filePath);
SpamWords = new HashSet<String>();
HamWords = new HashSet<String>();
LastLineIndicatorText = "# Ham training data";
Learn(true);
LastLineIndicatorText = "# Test data";
Learn(false);
LoadTestData();
}
private void Learn(bool isLearningSpam){
currentLine = spamDataFile.ReadLine();
// I know the first line is garbage so I throw it away
currentLine = spamDataFile.ReadLine();
while (currentLine != null && currentLine != LastLineIndicatorText){
var localS = currentLine.Trim("Spam,".ToCharArray()).Trim("Ham,".ToCharArray());
var words = localS.Split(new char[]{' '});
foreach (String word in words){
if (isLearningSpam){
SpamWords.Add(word.TrimEnd('.'));
}
else{
HamWords.Add(word.TrimEnd('.'));
}
}
//read next line in
currentLine = spamDataFile.ReadLine();
}
}
private void LoadTestData(){
AllTestData = new List<String>();
currentLine = spamDataFile.ReadLine();
while (currentLine != null){
AllTestData.Add(currentLine);
currentLine = spamDataFile.ReadLine();
}
}
public bool IsItSpam(string data){
var dataWords = data.Split(' ');
hamCounter = 0;
spamCounter = 0;
foreach (String token in dataWords){
if (SpamWords.Contains(token)){ spamCounter++;}
if (HamWords.Contains(token)){ hamCounter++;}
}
if (spamCounter >= hamCounter){
return true;
}
return false;
}
}
代码解释
LanguageLearner
类非常易于使用。
您所要做的就是通过将 SpamDetectionData.txt 文件的路径传递给它来构造一个 LanguageLearner
。
LanguageLearner ll = new LanguageLearner(@"c:\users\<username>\SpamDetectionData.txt");
当您这样做并调用构造函数时,它将自动为您解析文件。
以下是它所执行的步骤
- 设置两个
HashSet
(一个用于垃圾邮件词,另一个用于正常邮件(安全)词) - 调用
Learn()
方法,使用 SpamDetectionData.txt 中前 N 行(标记为垃圾邮件)训练自己学习垃圾邮件词 - 再次调用相同的
Learn()
方法,使用接下来的 N 行(标记为正常邮件)训练自己学习哪些词被认为是安全的 - 调用
LoadTestData()
加载 SpamDetectionData.txt 中标记为垃圾邮件或正常邮件的行。我将这些数据加载到List<String>
中,以便以后可以遍历它,让我的程序确定一行是垃圾邮件还是正常邮件。 - 完成所有这些操作后,我们可以调用
IstItSpam()
方法,该方法将尝试确定一行数据是垃圾邮件还是正常邮件。
使用代码测试数据
以下是使用代码测试数据的方法。这非常简单。
首先,我打印出代码执行的一些统计数据。
Console.WriteLine("Found {0} words.",ll.SpamWords.Count);
Console.WriteLine("Found {0} words.",ll.HamWords.Count);
Console.WriteLine(ll.AllTestData.Count);
结果如下(如这里在 LINQPad 中运行所示)
LanguageLearner
找到了 3374 个(垃圾邮件)词和 3419 个(正常邮件)词。
它还读取了 100 行测试数据。
IsItSpam() 方法:暴力破解
现在,我们将在测试数据上尝试我们的 IsItSpam()
方法。
我将其设置为可以非常轻松地将每行数据发送到我们的 IsItSpam()
方法中。
foreach (String testData in ll.AllTestData){
Console.Write("{0} : ",testData.Substring(0,4));
Console.Write(ll.IsItSpam(testData.Trim("Spam,".ToCharArray()).Trim("Ham,".ToCharArray())));
Console.WriteLine("\tSTATS : Ham weight = {0} Spam weight = {1}", ll.hamCounter, ll.spamCounter);
}
我知道 AllTestData
中的每一行都只是一个 string
,它表示来自文件 SpamDetectionData.txt 的测试数据中的每一行。
垃圾邮件或正常邮件
测试数据的创建者在每行前面都添加了
垃圾邮件,
正常邮件,
这样我们就可以确定我们的测试是否成功。
我的第一次调用 Console.Write
只是简单地打印出前缀和冒号 (:),以便您可以在屏幕上看到它。
接下来,我们从 string
中 Trim
掉 Spam
或 Ham
,并将实际数据传递给 IsItSpam()
方法。
当它是 Spam
时,该方法将返回 TRUE
,当它是 Ham
时,它将返回 FALSE
。
如果一切顺利,我们应该会看到如下输出行
正常邮件: False
垃圾邮件: True
如果所有行都以这种方式返回,那么我们已经成功地找到了 SPAM
,没有误报,并且我们已经通过暴力破解实现了这一目标。
IsItSpam() 做了什么?
这就是使用 IsItSpam
方法暴力破解数据是多么容易。
这是该方法再次出现
public bool IsItSpam(string data){
var dataWords = data.Split(' ');
int hamCounter = 0;
int spamCounter = 0;
foreach (String token in dataWords){
if (SpamWords.Contains(token)){ spamCounter++;}
if (HamWords.Contains(token)){ hamCounter++;}
}
if (spamCounter >= hamCounter){
return true;
}
return false;
}
我只是为 ham
词设置了一个计数器,为 spam
词设置了一个计数器。然后,当数据行传入时,我检查每个词是否存在于关联的词列表(SpamWords
和 HamWords
)中。
如果在列表中找到该词,则关联的计数器会递增。
最后,如果 spamCounter
大于或等于(因为那会是很多 spam
词),那么我将其视为 spam
并返回 true
。
否则,我们返回 false
。
结论:100% 成功
我很高兴地说,这种由完全没有受过 AI 或机器学习训练的人编写的暴力破解方法能够 100% 准确地确定数据中的正确 Spam
或 Ham
。
这很有趣,我希望它能说明有时“只管写出来”是值得的。:)
这是我的前几行输出的片段,数据在 LINQPad (http://linqpad.net) 中运行后。
脏数据(更新 1)
如果您更仔细地检查代码,您会发现学习到的数据(我的两个 HashSet
中的词)实际上相当脏。我做了一个非常快速的算法,它只是简单地按空格分割词,这并不完全正确。这就是我如此震惊(和着迷)于该算法足够好以至于 100% 获得正确测试数据的原因之一。
这让我现在真的在思考,如何创建这两个大词列表,然后对文本是否是 spam
有一个相当好的了解。这确实激发了我的想象力,我感谢 CP 编辑们提出这个挑战。很棒的东西。
正常邮件和垃圾邮件权重(更新 2)
我很好奇 ham
和 spam
的计数可能有多接近,但是代码以前没有提供一种简单的方法来获取这些统计数据。我想知道这些值是会非常接近还是相当遥远,所以我对 LanguageLearner
类做了一个非常小的更改,以公开 HamCounter
和 SpamCounter
值,以便我们可以在每次 IsItSpam()
方法运行时获取它们。
之后,我只是在程序的 main()
方法中添加了代码,以便在程序运行时显示该信息。
输出如下所示,现在您可以检查每个测试行的权重有多接近。再次,我惊讶于暴力破解方法和脏数据为每个值获得了如此遥远的值。我以为由于我的草率代码可能会有一些更接近的值。:)
附加结论
现在我已经解决了第一个难题,并且更好地理解了挑战,我觉得我可以去阅读一些关于机器学习和人工智能的理论,并更好地理解它们。
此外,在我解决这个问题之前,我感觉我无法解决第二个基于语言的挑战,但现在我感觉我至少可以尝试一下。
关于使用代码的注意事项
请确保您更改路径,使其指向您的 SpamDetectionData.txt 文件所在的位置。
否则,应用程序将崩溃。
历史
- 2018-02-06 晚上:添加了在程序中显示
Ham
和Spam
权重的能力,并在文章中添加了解释ham
/spam
权重的部分。还更新了所有相关的代码片段。 - 2018-02-06 稍后:编辑以添加有关脏数据使用的信息
- 2018-02-06:竞赛参赛作品首次发布