65.9K
CodeProject 正在变化。 阅读更多。
Home

暴力破解找到垃圾邮件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.47/5 (8投票s)

2018年2月6日

CPOL

6分钟阅读

viewsIcon

14163

downloadIcon

238

人工智能和机器学习竞赛的参赛作品。我就是这样学习/猜测如何查找垃圾邮件的。

引言

我对人工智能或机器学习一无所知。然而,当我看到与竞赛相关的数据文件时,我非常好奇如何确定测试数据中的哪些行是垃圾邮件。

我决定放弃所有关于该主题的研究,只是为了学习而暴力破解。

不到 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");

当您这样做并调用构造函数时,它将自动为您解析文件。

以下是它所执行的步骤

  1. 设置两个 HashSet(一个用于垃圾邮件词,另一个用于正常邮件(安全)词)
  2. 调用 Learn() 方法,使用 SpamDetectionData.txt 中前 N 行(标记为垃圾邮件)训练自己学习垃圾邮件
  3. 再次调用相同的 Learn() 方法,使用接下来的 N 行(标记为正常邮件)训练自己学习哪些词被认为是安全的
  4. 调用 LoadTestData() 加载 SpamDetectionData.txt 中标记为垃圾邮件正常邮件的行。我将这些数据加载到 List<String> 中,以便以后可以遍历它,让我的程序确定一行是垃圾邮件还是正常邮件
  5. 完成所有这些操作后,我们可以调用 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 的测试数据中的每一行。

垃圾邮件或正常邮件

测试数据的创建者在每行前面都添加了

  1. 垃圾邮件,
  2. 正常邮件,

这样我们就可以确定我们的测试是否成功。

我的第一次调用 Console.Write 只是简单地打印出前缀和冒号 (:),以便您可以在屏幕上看到它。

接下来,我们从 stringTrimSpamHam,并将实际数据传递给 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 词设置了一个计数器。然后,当数据行传入时,我检查每个词是否存在于关联的词列表(SpamWordsHamWords)中。

如果在列表中找到该词,则关联的计数器会递增。

最后,如果 spamCounter 大于或等于(因为那会是很多 spam 词),那么我将其视为 spam 并返回 true

否则,我们返回 false

结论:100% 成功

我很高兴地说,这种由完全没有受过 AI 或机器学习训练的人编写的暴力破解方法能够 100% 准确地确定数据中的正确 SpamHam

这很有趣,我希望它能说明有时“只管写出来”是值得的。:)

这是我的前几行输出的片段,数据在 LINQPad (http://linqpad.net) 中运行后。

脏数据(更新 1)

如果您更仔细地检查代码,您会发现学习到的数据(我的两个 HashSet 中的词)实际上相当脏。我做了一个非常快速的算法,它只是简单地按空格分割词,这并不完全正确。这就是我如此震惊(和着迷)于该算法足够好以至于 100% 获得正确测试数据的原因之一。

这让我现在真的在思考,如何创建这两个大词列表,然后对文本是否是 spam 有一个相当好的了解。这确实激发了我的想象力,我感谢 CP 编辑们提出这个挑战。很棒的东西。

正常邮件和垃圾邮件权重(更新 2)

我很好奇 hamspam 的计数可能有多接近,但是代码以前没有提供一种简单的方法来获取这些统计数据。我想知道这些值是会非常接近还是相当遥远,所以我对 LanguageLearner 类做了一个非常小的更改,以公开 HamCounterSpamCounter 值,以便我们可以在每次 IsItSpam() 方法运行时获取它们。

之后,我只是在程序的 main() 方法中添加了代码,以便在程序运行时显示该信息。

输出如下所示,现在您可以检查每个测试行的权重有多接近。再次,我惊讶于暴力破解方法和脏数据为每个值获得了如此遥远的值。我以为由于我的草率代码可能会有一些更接近的值。:)

附加结论

现在我已经解决了第一个难题,并且更好地理解了挑战,我觉得我可以去阅读一些关于机器学习和人工智能的理论,并更好地理解它们。

此外,在我解决这个问题之前,我感觉我无法解决第二个基于语言的挑战,但现在我感觉我至少可以尝试一下。

关于使用代码的注意事项

请确保您更改路径,使其指向您的 SpamDetectionData.txt 文件所在的位置。

否则,应用程序将崩溃。

历史

  • 2018-02-06 晚上:添加了在程序中显示 HamSpam 权重的能力,并在文章中添加了解释 ham / spam 权重的部分。还更新了所有相关的代码片段。
  • 2018-02-06 稍后:编辑以添加有关脏数据使用的信息
  • 2018-02-06:竞赛参赛作品首次发布
© . All rights reserved.