机器学习代码的单元测试

31

我正在为我的计算机视觉论文编写一款相当复杂的机器学习程序。它运行得相当好,但我需要不断尝试新的东西并添加新功能。这是有问题的,因为当我扩展代码或尝试简化算法时,有时会引入错误。

显然,正确的做法是添加单元测试,但如何做到这一点尚不清楚。我的程序的许多组件产生了一些主观答案,我无法自动进行健全性检查。

例如,我有一些代码,用低分辨率曲线逼近曲线,以便在低分辨率曲线上进行计算密集型工作。我意外地在这段代码中引入了一个错误,并且只有在我的整个程序结果稍微变差时才发现了它。

但是,当我试图为它编写单元测试时,不清楚我该做什么。如果我制作一个具有明确正确低分辨率版本的简单曲线,那么我并没有真正测试出可能出错的所有内容。如果我制作一个简单的曲线,然后稍微改变其点,我的代码开始产生不同的答案,即使这个特定代码现在似乎运行良好。

6个回答

13
你可能不会意识到其讽刺意味,但基本上你手头的代码是遗留代码:一块没有任何单元测试的软件。自然而然你不知道从哪里开始。所以,阅读有关处理遗留代码的信息可能会对你有所帮助。
其中权威的参考是Michael Feather的书《与遗留代码有效地工作》。曾经在ObjectMentor网站上有这本书的一个有用摘要,但不幸的是该网站已经消失了。然而,WELC在评论和其他文章中留下了遗产。查看它们(或直接购买该书),虽然S.Lott和tvanfosson在他们的回复中涵盖的关键教训才是最重要的。

2019年更新:我已经修复了指向WELC摘要的链接,使用来自Wayback Machine网站存档的版本(感谢@milia)。

此外-尽管知道主要包含其他网站链接的答案是低质量答案:) - 这里是一个链接2019年新版的Google测试和调试ML代码教程。我希望这对未来遇到这个问题的人有所启发。


这实际上是最有用的建议。我所有成功的调试都是通过手动使用这样的技术实现的。但是这个PDF提供了一些自动化该过程的好建议。您的PDF链接对我无效,但是简单的谷歌搜索可以找到它。 - forefinger
@forefinger - 我已经修复了链接。但我很高兴你找到了这篇文章,并且觉得它有用。 - APC
2
那份PDF的作者现在有一本关于同一主题的优秀书籍:http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052 - TrueWill
@APC,链接似乎又出问题了。 - Giacomo
@giac_man - 是的,看起来ObjectMentor网站已经挂了。似乎没有其他地方有链接文章的副本。也许Wayback Machine有它,但是当我刚才尝试时它一直在给DNS错误 :-( - APC
@APC 这是链接:https://web.archive.org/web/20170110132026/https://www.netobjectives.com/system/files/WorkingEffectivelyWithLegacyCode.pdf - milia

12

"那么我并没有真正测试出所有可能出现的问题。"

正确。

单元测试的工作不是测试所有可能出现的问题,而是测试在给定特定输入和期望结果的情况下,你的代码是否做了正确的事情。重要的是确保特定可见的、外部的需求被特定的测试用例满足,而不是每一个可能出现的问题都被防止。

无法测试出所有可能出现的问题。你可以写一个证明,但很难为每个可能性编写测试用例。

明智地选择你的测试用例。

此外,单元测试的工作是测试整个应用程序中每个小部分是否在隔离状态下完成了正确的任务。

例如,你的“用低分辨率曲线逼近曲线的代码”,可能有几个小部分可以作为单独的单元进行测试,这样就能确保整体也能正常工作。

例如,你的“在低分辨率曲线上进行密集计算的代码”,可能有几个小部分可以作为单独的单元进行测试,在隔离状态下执行。

单元测试的目的是创建小的、正确的单元,以后再将它们组装起来。


这似乎是合理的建议,但它并没有像其他回复那样对我的具体问题有太大帮助。 - forefinger
1
“specific issues”?这不太容易理解,因为您的问题似乎没有列出任何具体问题。如果您需要更多信息,请随时更新您的问题。 - S.Lott

11

没有看到你的代码,很难确定,但我怀疑你试图在太高的层次上编写测试。您可能需要考虑将方法分解为更小的组件,这些组件是确定性的,并对其进行测试。然后通过提供返回基础方法(可能位于不同对象上)的可预测值的模拟实现来测试使用这些方法的方法。然后,您可以编写覆盖各种方法领域的测试,确保您具有可能结果的全范围覆盖率。对于小的方法,您可以通过提供代表输入域的值来实现,而对于这些方法所依赖的方法,则可以通过提供返回依赖项的结果范围的模拟实现来实现。


这个建议很有帮助。在这个例子中,我通过动态规划来进行近似。这可以分解成几个确定性的组件:
  1. 计算近似的特定部分的误差。我可以手动为某些特定曲线完成这个步骤。
  2. 确保整体目标函数是正确的。同样,我可以手动完成这个步骤。
  3. 确保动态规划是正确的。(这就是错误所在的地方。)
- forefinger
1
如果我知道整体目标函数是正确的,我可以通过提供已被扰动的简单可分解曲线来测试它。只要它给出的答案比我原本想要的答案得分更高,那么动态规划可能就是正确的。 - forefinger

7

您的单元测试需要采用某种模糊因素,可以接受近似值,或使用某种概率检查。

例如,如果您有一些返回浮点结果的函数,几乎不可能编写适用于所有平台的正确工作的测试。您的检查需要执行近似计算。

TEST_ALMOST_EQ(result, 4.0);

以上的TEST_ALMOST_EQ可能会验证result是否在3.9到4.1之间(例如)。

另外,如果您的机器学习算法是概率性的,则您的测试需要适应它,通过多次运行的平均值,并期望其在某个范围内。

x = 0;
for (100 times) {
  x += result_probabilistic_test();
}

avg = x/100;
TEST_RANGE(avg, 10.0, 15.0);

当然,测试是不确定的,所以您需要调整它们,以便您可以获得高概率的非易错测试(例如,增加试验次数或增加误差范围)。
您还可以使用模拟对象来实现这个目标(例如,一个模拟随机数发生器用于您的概率算法),并且它们通常有助于以确定性方式测试特定的代码路径,但是维护它们需要付出很多努力。理想情况下,您应该同时使用模糊测试和模拟对象。
希望对您有帮助。

这是很好的建议,但它并不能真正解决我的问题,因为我需要检查的许多事物都是离散的,所以无法计算出误差,并且它们不能有意义地平均。 - forefinger

1
通常,对于统计量,您会为答案建立一个 epsilon。例如,您的点的平均平方差应该小于0.01或类似值。另一个选择是多次运行,如果失败的次数“太多”,那么就存在问题。

0
  1. 获取一个适当的测试数据集(可能是通常使用的子集)
  2. 在此数据集上计算某些指标(例如准确度)
  3. 记录所得到的值(交叉验证)
  4. 这应该给出设置阈值的指示

当然,如果对代码进行更改时,数据集上的性能可能会略微提高,但如果性能大幅下降,则表明出现了问题。


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