Logo
Section 1 / 5

运行时检查

了解断言及其使用时机。

导入

运行时检查

了解断言及其使用时机。

概述

当我们的机器学习服务代码编写完成并投入生产环境后,它应当像一个有机生命体一样平稳运行。我们并不总有机会实时介入并修复问题。要让代码具备可靠性,可做的事情并不多,但本节将讨论其中大部分关键实践。

快速失败 vs. 安全容错

根据具体领域和应用场景的不同,我们应选择对异常或缺陷进行静默处理(记录日志或触发告警后继续运行),还是直接终止程序运行。这一决策也被称为“健壮性”与“正确性”之间的权衡问题。

健壮性 vs. 正确性

Steve McConnell 在《代码大全》(Code Complete)中精辟地指出了健壮性与正确性的区别:

“正如视频游戏和X光设备的例子所示,最合适的错误处理方式取决于出错的软件类型。这些例子还说明,错误处理通常倾向于更注重正确性或更注重健壮性。开发者常非正式地使用这些术语,但从严格意义上讲,这两个概念处于对立的两端。正确性意味着绝不返回不准确的结果;宁可不返回结果,也不返回错误结果。健壮性则意味着始终尝试做些事情以使软件持续运行,即使这会导致偶尔产生不准确的结果。

...

安全关键型应用通常优先考虑正确性而非健壮性。不返回结果比返回错误结果更好。放射治疗设备就是这一原则的良好例证。 消费类应用则往往更重视健壮性而非正确性。只要软件不停机,任何结果通常都优于完全崩溃。比如我现在使用的文字处理器,偶尔会在屏幕底部显示半行文本。如果检测到这种情况,我希望这个程序直接关闭吗?”

在设计和实现任何系统时,都必须认真权衡健壮性与正确性,并评估错误或故障可能带来的后果。

对于安全关键型应用,通常更应优先保证正确性;而在消费类应用中,则往往更需要优先保障健壮性,因为用户通常更能容忍一些小瑕疵或错误,只要系统整体仍能正常运行即可。

机器学习中的示例

这一原则同样适用于机器学习流水线。假设我们正在开发一款娱乐类应用,例如 Snapchat 或 TikTok 中的 AR 面部特效。最坏情况下,某个帧上的特效显示效果不佳,这种风险并不严重,因此在此场景下追求健壮性是合理的选择。

相反,如果是用于医疗诊断或交通运输的机器学习解决方案呢?我们是否希望一辆自动驾驶汽车在无法确定附近是否有行人时仍然继续前进?

断言与运行时检查

尽管断言在测试阶段非常有用,但在运行时应优先使用条件判断而非断言。原因如下:

  • Python 的优化模式(如 python -O script.py)会忽略断言,某些特殊解释器也可能不支持;
  • 断言提供的错误信息不够详细;
  • 遵循《Python之禅》的原则:显式优于隐式。

下面我们来看条件检查和断言的示例。首先是条件检查:

1if not input_data.shape == (100, 100, 3): 2 logger.error(f"输入数据形状为 {input_data.shape},期望形状为 (100, 100, 3)") 3 raise SpecificRuntimeError("适当的错误提示")

这里我们检查输入是否符合预期形状,若不符合则抛出特定类型的运行时异常,并附带清晰的错误消息。

再看断言的写法:

1msg = f"输入数据形状为 {input_data.shape},期望形状为 (100, 100, 3)" 2assert input_data.shape == (100, 100, 3), msg

断言只能抛出 AssertionError,并可选地附带一条错误信息。

虽然条件检查看起来更冗长,但在这种场景下,适当的冗长是有益的,有助于在出错时快速定位问题。

优化如何影响断言?

以下是一个包含断言的代码示例。请思考:如果我们执行下面这条命令,输出会是什么?

1subprocess.run(["python", "-O", "check.py"])

然后移除 -O 标志再运行一次,观察输出变化。

是否应在运行时使用断言?

另一种常见做法是:避免在运行时抛出任何异常(但在测试阶段允许抛出 AssertionError)。

在前面提到的“快速失败 vs. 安全容错”部分中,我们讨论过,数据中一些微小的意外问题可能导致整个服务崩溃,而这并非我们所期望的。我们更希望服务能够继续运行,并通知我们捕获到的异常。因此,与其直接抛出错误,不如采取以下策略:

  • 在运行时进行详细的日志记录;
  • 返回默认值作为机器学习服务的响应(即所谓的默认回退机制)。

在许多应用机器学习的领域中,通常存在非机器学习的基线方案(例如基于规则的方法或线性模型),或者可以接受空输出(如未检测到目标对象)。

以下是如何实现此类行为的示例:

1from ctr import ctr 2import os 3 4os.environ["CHECKING_MODE"] = "prod" 5response1 = ctr({"n_views": 30, "n_clicks": 14}) 6print("response1", response1) 7 8os.environ["CHECKING_MODE"] = "prod" 9response2 = ctr({"n_views": 0, "n_clicks": 0}) 10print("response2", response2) 11 12os.environ["CHECKING_MODE"] = "prod" 13response3 = ctr({"n_views": 10, "n_clicks": 666}) 14print("response3", response3) 15 16os.environ["CHECKING_MODE"] = "test" 17response4 = ctr({"n_views": 20, "n_clicks": 13}) 18print("response4", response4) 19 20os.environ["CHECKING_MODE"] = "test" 21response5 = ctr({"n_views": -1, "n_clicks": 2}) 22print("response5", response5)

说明

上述代码中,ctr.py 文件中的 ctr 函数接收一个特征字典,计算并返回点击率(CTR),即点击数与浏览数的比值。

在 utils.py 中定义了 assert_safe_run 装饰器,它包装目标函数(如 ctr)。当发生错误时,在生产模式(prod)下仅记录警告日志(logging.warn),而在测试或调试模式(test)下则主动抛出异常。

错误可能出现在两种情况:一是没有浏览量(此时CTR无定义),二是点击数超过浏览数——如果数据采集正确,这两种情况都不应出现。

注意:在决定你的机器学习服务在运行时应如何处理异常和断言错误时,请始终牢记健壮性与正确性之间的权衡。

运行时应该检查哪些内容?

简短的回答是:所有你脑海中浮现的隐式假设。以下是一些启发性的检查项:

  • 输入数据格式(如图像:通道数、BCHW vs. BHWC vs. HWC);
  • 输入数据类型(如 float32 vs. uint8);
  • 输入数据范围(如图像像素值在 0..255 或 0..1 范围内);
  • 其他领域相关的常量和不变量;
  • 类似属性的校验(例如图像旋转函数可检查旋转矩阵的行列式是否等于 1,否则该矩阵不仅包含旋转,还涉及缩放,因此不是纯旋转操作)。
1def rotate_image(image, rotation_matrix): 2 det = np.linalg.det(rotation_matrix) 3 if np.abs(det - 1) > 1e-6: 4 raise ValueError(f'旋转矩阵 {rotation_matrix.tolist()} 包含缩放') 5 ...

特定于机器学习的测试

类型提示