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

4
众所周知,贝叶斯分类器是过滤垃圾邮件的有效方法。这些分类器可以相当简洁(我们的分类器只有几百行代码),但所有核心代码都需要事先编写,才能得到任何结果。
然而,测试驱动开发(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应该如何运作呢?(特别是如果事先已知最佳实现不能轻易地演变)

9个回答

4

从写垃圾邮件过滤算法的低级部分开始编写测试。

首先,你需要在脑海中构思出算法的大致设计,然后将算法的核心部分隔离出来,并为其编写测试。对于垃圾邮件过滤器来说,可能是使用贝叶斯定理计算一些简单概率(我不了解贝叶斯分类器,所以可能有误)。你需要逐步地自下而上构建它,直到最终实现所有算法部分,并将它们组合起来变得简单。

需要大量练习才能知道要按哪个顺序编写测试,以便可以采用足够小的步骤进行TDD。如果你需要编写超过10行代码才能通过一个新的测试,那么你可能做错了什么。从更小的东西开始或模拟一些依赖关系会更安全。错误地朝着更小的方向前进,使步骤太小,进展缓慢,总比尝试迈出太大的步伐而失败要好。

你所举的“廉价的通用万艾可(Cheap generic viagra)”例子也许更适合于验收测试。它可能会运行得非常缓慢,因为你首先需要初始化带有示例数据的垃圾邮件过滤器,所以它不适用于TDD测试。TDD测试需要FIRST(快速,例如每秒数百或数千次)。


2
这是我的看法:测试驱动开发意味着在编写代码之前编写测试。这并不意味着为每个需要编写测试的代码单元都必须是微不足道的。
此外,您仍然需要计划软件以合理有效的方式执行其任务。对于这个问题来说,简单地添加越来越多的字符串似乎不是最好的设计方法。
因此,简言之,您应该从最小的功能片段开始编写代码(并进行测试),但不要以这种方式设计算法(使用伪代码或您喜欢的任何方式)。
很有意思看到你和其他人是否同意。

0
您所描述的问题是理论性的,即通过针对测试添加冗余代码会使得代码变得混乱不堪。您所遗漏的非常重要。
开发循环是:红-绿-重构。
您不能只是在红和绿之间来回跳动。一旦您的测试通过(绿色),就应该重构生产代码和测试。然后编写下一个失败的测试(红色)。
如果您正在重构,则应该消除复制、混乱和不精确性。在代码增长时,您将迅速转向提取方法,构建评分和评级,并可能引入外部工具。你会尽快这样做,因为这是最简单的工作。
不要只是在红和绿之间跳动,否则所有的代码都会变成垃圾。重构步骤不是可选的或随意的,而是必不可少的。

0

我认为检查特定字符串是否为垃圾邮件并不是真正的单元测试,这更像是客户测试。这里有一个重要的区别,因为它不是真正的红色/绿色类型的事情。实际上,您应该有几百个测试文档。最初,一些文档将被归类为垃圾邮件,随着您改进产品,分类将更直接地符合您想要的内容。所以,您应该制作一个自定义应用程序来加载一堆测试文档,对其进行分类,然后全面评估得分。完成此客户测试后,由于您尚未实施算法,得分将非常低。但是,您现在有了衡量前进进度的手段,考虑到您可以预期的大量学习/更改/实验,这非常有价值。

当您实施算法时(甚至是首次进行客户测试时),仍然可以进行具有实际单元测试的TDD。贝叶斯过滤器组件的第一个测试不会衡量特定字符串是否评估为垃圾邮件,而是衡量字符串是否适当通过贝叶斯过滤器组件传递。您接下来的测试将集中于如何实现贝叶斯过滤器(正确结构化节点,应用训练数据等)。

在编程方面,你需要有一个关于产品发展方向的愿景,你的测试和实现应该朝着这个愿景的方向进行。你不能盲目地添加客户测试,你需要考虑整体产品愿景来添加测试。任何软件开发目标都会有好的测试和坏的测试可以编写。


0

对我来说,你所谓的最小化代码通过测试就是整个 IsSpam() 函数。这与其大小一致(你说只有几百行代码)。

或者,渐进式方法并不要求先编码再思考。你可以设计解决方案,编写代码,然后使用特殊情况或更好的算法来完善设计。

无论如何,重构并不仅仅是在旧代码上添加新内容。对我来说,这是一种更破坏性的方法,你会为了一个简单的功能丢弃旧代码,并用一个更精细、更复杂的功能替换它。


0

你已经有了单元测试,对吧?

这意味着你现在可以重构代码甚至重写它,并使用单元测试来查看是否出现了问题。

先让它工作,然后再让它变得简洁 -- 现在是第二步了 :)


0

(1) 你不能像判断一个数字是否为质数那样,简单地说一个字符串“是垃圾邮件”或“不是垃圾邮件”。这并不是非黑即白的。

(2) 使用测试用例中的示例来编写字符串处理函数是错误的,当然也不是TDD的目的。示例应该代表一种值。TDD不能防止愚蠢的实现,所以你不应该假装自己完全不知道,也不应该写return text == "Cheap generic viagra"


我使用的例子是从我看到的很多人认为TDD应该这样做的例子中取出来的,它们都是按照这种方式编写最少量的代码以使测试通过。 - Greg Beech
@Greg:对我来说,就算是起点也必须有一定的意义。我不会以“返回number == 17”并声称“它适用于已测试的输入数据子集”来开始实现bool IsPrime(int number) const。或许很多人都会支持这种策略,但我会考虑在有争议的观点问题中添加我的意见作为答案。 - Daniel Daranas
@Daniel:我不会有问题这样做:当你添加案例“19”时,你可以概括答案(也许是(number%2 == 1))。之后,添加2和/或9个案例。用这种方式做,最终你会得到正确的算法。 - Lennaert
2
@Daniel - 实质上你回到了我的问题……为什么人们会演化出一种已知不是解决问题的最佳方式的解决方案呢?所以看起来你同意我认为“编写能通过测试的最简单的东西”并不是一个明智的哲学,尽管这是“纯”TDD似乎要求的。 - Greg Beech
@Greg,是的,我不同意对“写最简单的能通过测试的代码”进行字面解释。 我会提出一条替代而更好的规则,例如(1.a)“您编写的所有代码应遵循合理的逻辑假设,以实现指定函数目标(如情况所述)”,(1.b)“这不包括针对输入的特定值进行任何个性化处理”,以及(2)“编写最简单的代码来满足(1),并通过测试”。即使这也只是一个通用的指导方针。 - Daniel Daranas
显示剩余3条评论

0

在贝叶斯垃圾邮件过滤器中,我认为你应该使用现有的方法。特别是你将使用贝叶斯定理和可能还会用到其他概率论。

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

关键是,你的单元测试不是为了测试你的算法是否合理。你应该已经知道这一点,或者你的程序可能是作为一个实验来设计的,看看它是否合理。

这并不意味着ispam函数的性能不重要。但它不必成为单元测试的一部分。数据可以来自alpha测试的反馈、新的理论结果或你自己的实验。在这种情况下,可能需要一个新的算法,并且需要新的单元测试。

另请参见this question关于测试随机数生成器的问题。


0

问题不在于测试驱动开发,而在于你的测试。如果你从一个单一的测试开始开发代码,那么所有的测试都只是在指定一个字符串检查函数。

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

实际上,你可能无法通过100%的单元测试,因此你只需要尽可能通过尽可能多的测试。你还必须确保你的测试足够真实。如果你用这种方式来思考,测试驱动开发和机器学习有很多共同点。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接