为什么测试全绿,程序还是错了?
Gary Bernhardt 的隔离测试和 Andrej Karpathy 的训练演示里,代码出错时,屏幕上都可能传来好消息。
改完代码,测试全绿。你有理由比刚才更放心。可如果有人问「哪种错误即使还在,这些测试也会绿?」,你会想到什么?两位讲者各给了一个容易漏掉的答案。
测试替身和代码可以一起搞错
Gary Bernhardt 在《Boundaries》里讲了一个例子。程序要找出到期的用户,再发出邮件。为了单独测试这段流程,可以用一个替身代替真实的用户查询。你预先告诉它会查到谁,测试就能很快检查下一步有没有调用邮件服务。
这里藏着一个循环:写代码的人对真实接口有一个理解,又按同一个理解写了替身。如果理解错了,代码和替身依然可以配合得天衣无缝。绿色结果告诉你流程在这个约定下能走通;真正的用户查询是否也遵守这个约定,还得去另一处看。
if you don't stub the boundary correctly, your test will pass and your production system will be wrong.回看原片 4:14–4:25 →
连真实运行的程序,也可能给你好消息
看完第一个例子,很容易想到「那我把真的程序跑一遍」。Karpathy 恰好这么做了。他亲手写了一个小型神经网络,训练时用梯度决定每个参数往哪个方向调。预测越来越接近答案,训练误差降得很快。然后他指出了代码里的一处错误。
这段训练本该在每轮重新计算前把旧梯度清零,程序却把上一轮的数值也加了进来。这个小问题恰好很容易学,累加让参数迈出了更大的步子。修好之后,误差反而降得慢了一些。只看「误差有没有下降」,两个版本都会过关,错误版本甚至更讨喜。
you may have lots of bugs in the code and uh your network might actually work回看原片 2:13:44–2:13:51 →
把「误差下降」换成一个能算错的预言
上面的小实验里,第二轮开始时参数是 3。判断实现是否正确,可以先写下预言:此刻的损失是 w²/2,对当前参数的导数应当是 3。它不是「大概朝下」的方向,而是一个具体数值。反向传播的微积分讲解把同一检查推广到网络:沿一条计算路径,把几段局部变化率相乘,才得到当前权重对损失的影响。
This right here is the chain rule, where multiplying together these three ratios gives us the sensitivity of c to small changes in WL.回看原片 3:45–3:55 →
现在再看累积出的 7。它也能让参数朝零走,所以损失下降的测试会放行;但它不等于刚刚手算的 3。这一次,失败能指向具体原因:使用的量含有上一轮的贡献。这和故意跨多个样本累积梯度是两种不同的更新约定。
让你怀疑的错误有机会露面
把两段放在一起看,第一处要追问的是「换回真实依赖,这个约定还成立吗?」;第二处已经跑了真实代码,要追问的是「照这个标准,做错了也能通过吗?」这两种疑问需要不同的检查。
下次评审代码时,可以挑一条正在通过的测试,试着说出一个它应该抓住的具体错误。比如「用户查询返回的结果和替身约定的格式不同」,那就用一次真实查询对照两者。换成「第二轮更新混入了第一轮的梯度」,那就挑一个能手算两轮的小例子,直接核对第二轮的更新值。再有意引入你怀疑的错误,看看检查会不会变红。
同样是一盏绿灯,这时你能更准确地说出自己有把握的那件事。