TDD 和贝叶斯垃圾邮件过滤器问题

Posted

技术标签:

【中文标题】TDD 和贝叶斯垃圾邮件过滤器问题【英文标题】:TDD and the Bayesian Spam Filter problem 【发布时间】:2010-10-20 12:39:32 【问题描述】:

众所周知,贝叶斯分类器是过滤垃圾邮件的有效方法。这些可以相当简洁(我们的只有几百个 LoC),但在获得任何结果之前,所有核心代码都需要预先编写。

但是,TDD 方法要求只能编写通过测试的最少代码量,因此给出以下方法签名:

bool IsSpam(string text)

还有下面的一串文字,很明显是垃圾邮件:

"Cheap generic viagra"

我能写的最少代码是:

bool IsSpam(string text)

    return text == "Cheap generic viagra"

现在也许我添加另一个测试消息,例如

"Online viagra pharmacy"

我可以将代码更改为:

bool IsSpam(string text)

    return text.Contains("viagra");

...等等,等等。直到某个时候,代码变成一堆字符串检查、正则表达式等,因为我们已经进化了它,而不是从一开始就考虑它并以不同的方式编写它。

那么,TDD 应该如何处理这种从最简单的代码发展代码以通过测试的情况不是正确的方法? (特别是如果事先知道最好的实现不能简单地进化)。

【问题讨论】:

【参考方案1】:

首先为垃圾邮件过滤算法的较低级别部分编写测试。

首先,您需要对算法应该如何进行粗略设计。然后你隔离算法的核心部分并为它编写测试。在垃圾邮件过滤器的情况下,它可能会使用贝叶斯定理计算一些简单的概率(我不知道贝叶斯分类器,所以我可能是错的)。您自下而上逐步构建它,直到最终实现算法的所有部分并将它们组合在一起很简单。

需要大量练习才能知道以什么顺序编写哪些测试,以便您可以以足够小的步骤进行 TDD。如果您需要编写超过 10 行代码才能通过一项新测试,那么您可能做错了什么。从较小的东西开始或模拟一些依赖项。在较小的方面犯错更安全,这样步骤太小而您的进度很慢,而不是尝试迈出太大的步骤并严重失败。

您的“廉价通用伟哥”示例可能更适合acceptance test。它甚至可能会运行得很慢,因为您首先需要使用示例数据初始化垃圾邮件过滤器,因此它不会用作 TDD 测试。 TDD 测试需要FIRST(F = 快速,每秒有数百或数千次测试)。

【讨论】:

【参考方案2】:

这是我的看法:测试驱动开发意味着在编码之前编写测试。这并不意味着您为其编写测试的每个代码单元都必须是微不足道的。

此外,您还需要规划您的软件,以便以明智和有效的方式完成任务。简单地添加越来越多的字符串似乎并不是解决这个问题的最佳设计。

因此,简而言之,您从可能的最小功能块编写代码(并对其进行测试),但您不会以这种方式设计您的算法(在伪代码中或您喜欢这样做)。

看看你和其他人是否同意会很有趣。

【讨论】:

【参考方案3】:

对我来说,您所说的通过测试的最少代码量就是整个IsSpam() 函数。这与它的大小是一致的(你说只有几百个 LoC)。

另外,增量方法并不声称要先编码,然后再思考。您可以设计一个解决方案,对其进行编码,然后使用特殊情况或更好的算法来改进设计。

无论如何,重构并不只是简单地在旧内容上添加新内容。对我来说,这是一种更具破坏性的方法,您可以丢弃旧代码以获得简单的功能,并用新代码替换它以获得更精细和更精细的功能。

【讨论】:

【参考方案4】:

你有你的单元测试,对吧?

这意味着您现在可以重构代码甚至重写它并使用单元测试来查看您是否破坏了某些内容。

先让它工作,再让它干净 -- 是时候进行第二步了:)

【讨论】:

【参考方案5】:

(1) 你不能说一个字符串“是垃圾邮件”或“不是垃圾邮件”,就像你说一个数字是否是素数一样。这不是黑色或白色。

(2) 仅使用用于测试的示例来编写字符串处理函数是不正确的,当然也不是 TDD 的目标。例子应该代表一种价值观。 TDD 并不能防止愚蠢的实现,所以你不应该假装你根本不知道,所以你不应该写return text == "Cheap generic viagra"

【讨论】:

同意(1)。 Re (2):我相信TDD的目标是以这种方式开始编写功能;任何使测试绿色的东西。一段时间后,您会注意到模式并且可以重构代码。测试到位后,您应该立即知道您是否破坏了任何东西。如果您没有注意到任何模式,那么要么没有模式,因此没有可提取的内容,或者(更有可能)您的测试套件不完整。添加测试并再次查看。 我使用的示例取自我看到的许多关于人们认为应该如何完成 TDD 的示例,它们正是这样做的——编写最少的代码以使测试通过。 @Greg:对我来说,即使是起点也必须有些有意义。我不会开始使用“return number == 17”来实现 bool IsPrime(int number) const 并声称“它适用于经过测试的输入数据子集”。也许很多人为这个策略辩护,但我会考虑在有争议的意见问题中添加我的意见作为答案。 @Daniel:那样做我不会有问题:当您添加案例“19”时,您可以概括答案(也许是(数字%2 == 1))。之后,添加案例 2 和/或 9。这样做,您最终会得到正确的算法。 @Daniel - 你基本上回到了我的问题......为什么人们会在已知不是解决问题的最佳方法时发展解决方案?因此,您似乎同意我的观点,即“编写能够通过测试的最简单的东西”并不是一个明智的哲学,尽管这似乎是“纯”TDD 的要求。【参考方案6】:

在我看来,使用贝叶斯垃圾邮件过滤器,您应该使用现有方法。特别是你会使用贝叶斯定理,可能还有其他一些概率论。

在这种情况下,似乎最好的方法是根据这些方法来决定您的算法,这些方法应该经过试验和测试,或者可能是实验性的。然后,您的单元测试应该设计为测试 ispam 是否正确实现了您决定的算法,以及结果在 0 和 1 之间的基本测试。

关键是,您的单元测试并非旨在测试您的算法是否合理。您应该已经知道,或者您的程序可能被设计成一个实验,看看它是否明智。

这并不是说 isspam 功能的性能不重要。但它不一定是单元测试的一部分。数据可能来自 alpha 测试的反馈、新的理论结果或您自己的实验。在这种情况下,可能需要一种新算法,并且需要新的单元测试。

另请参阅this question,了解有关测试随机数生成器的信息。

【讨论】:

【参考方案7】:

这里的问题不在于测试驱动开发,而在于您的测试。如果您开始针对单个测试开发代码,那么您所做的所有测试就是指定一个字符串检查函数。

TDD 的主要思想是在编写代码之前考虑您的测试。您无法详尽地测试垃圾邮件过滤器,但您可以通过数万或数十万个测试文档得出一个合理的近似值。在存在这么多测试的情况下,朴素贝叶斯算法比十万行 switch 语句更简单。

实际上,您可能无法通过 100% 的单元测试,因此您只需要尝试尽可能多地通过即可。您还必须确保您的测试足够真实。如果你这样想,测试驱动开发和机器学习有很多共同点。

【讨论】:

【参考方案8】:

您所描述的问题是理论上的,通过在测试中添加杂乱无章的内容,您将制造出一个又大又乱的泥球。你缺少的东西很重要。

循环是:红色-->绿色-->重构

您不只是在红色和绿色之间跳跃。一旦测试通过(绿色),您就可以重构生产代码和测试。然后你编写下一个失败的测试(红色)。

如果你在重构,那么你就是在消除重复和混乱,并且随着它的增长而变得草率。您将很快达到提取方法、建立评分和评级以及可能引入外部工具的地步。只要它是最简单的,你就会去做。

不要只是在红色和绿色之间跳来跳去,否则你的所有代码都会变得一团糟。该重构步骤不是可选的或任意的。这是必不可少的。

【讨论】:

【参考方案9】:

我不认为检查特定字符串是否是垃圾邮件真的是单元测试,它更多的是客户测试。有一个重要的区别,因为它不是真正的红色/贪婪类型的东西。实际上,您可能应该有几百个测试文档。最初,有些会被归类为垃圾邮件,随着您对产品的改进,这些分类将更直接地符合您的要求。因此,您应该制作一个自定义应用程序来加载一堆测试文档,对它们进行分类,然后评估整体得分。当您完成该客户测试时,由于您尚未实施算法,因此得分将非常糟糕。但是您现在有了衡量前进进度的方法,考虑到您可以预期前进的学习/更改/实验的数量,这非常有价值。

当您实现算法时(甚至是第一手的客户测试),您仍然可以使用真正的单元测试来进行 TDD。贝叶斯过滤器组件的第一个测试不会测量特定字符串是否被评估为垃圾邮件,而是衡量该字符串是否适当地通过了贝叶斯过滤器组件。然后,您的下一个测试将关注如何实现贝叶斯过滤器(正确构建节点、应用训练数据等)。

您确实需要对产品的发展方向有一个愿景,并且您的测试和实施应该针对该愿景。您也不能只是盲目地添加客户测试,您需要在考虑整体产品愿景的情况下添加测试。任何软件开发目标都会有你可以编写的好测试和坏测试。

【讨论】:

以上是关于TDD 和贝叶斯垃圾邮件过滤器问题的主要内容,如果未能解决你的问题,请参考以下文章

在贝叶斯垃圾邮件过滤器中计算令牌成为垃圾邮件的概率

用于 Python 的贝叶斯垃圾邮件过滤库

如何用 Java 利用贝叶斯算法实现垃圾邮件过滤

贝叶斯推断及其互联网应用:过滤垃圾邮件

朴素贝叶斯垃圾邮件过滤问题

实例讲解:基于贝叶斯分类的垃圾邮件识别