当一个机器学习系统不工作时,通常有三个可能的原因:坏数据坏模型坏推断。以下是这三个类别下各自一些常见的问题成因:

  • 坏数据(Bad data):数据项被错误地录入、存储或加载;数据项不完整或标注错误;数据值噪声太大而无用;数据有偏或不能代表系统将被使用的方式;这是任务的错误数据;数据不足以做出准确预测。
  • 坏模型(Bad model):一个或多个建模假设是错的——即与实际生成数据的过程不一致;模型做了太多简化假设;给定可用数据量,模型包含的假设不足以做出准确预测。
  • 坏推断(Bad inference)推断代码含有 bug;消息传递调度不好;推断尚未收敛;存在数值问题(例如舍入、溢出);近似推断算法不够准确。

在我们的情形中,我们可以相当有信心地认为数据是好的,因为我们已经仔细地检查和可视化过它。所以看起来问题很可能出在模型推断上。我们将从检查推断算法——即循环置信传播——是否正确工作开始。

检查推断算法

为了看出推断是否正确工作,我们需要能够把推断问题所引起的任何毛病,与我们的模型不匹配数据所引起的任何毛病区分开来。为了实现这种区分,我们可以生成一个新的合成数据集,它保证与模型完全匹配。如果我们用这个数据集也得到糟糕的结果,那就表明存在推断问题。我们将通过从模型所指定的联合分布采样来创建这个合成数据集,这保证了数据与模型一致(关于采样是什么,可回顾第 1 章)。我们可以通过运行模型所指定的数据生成过程来产生样本——这个过程称为祖先采样(ancestral sampling),如算法 2.3 所定义(另见 Bishop [2006])。

算法 2.3:祖先采样

输入因子图

输出:图中每个变量的采样值

将变量从上到下排序,使得父变量排在子变量之前。

for each 该顺序中的变量 $v$ do

如果 $v$ 有父变量,取回它们的采样值(由于排序的缘故,这些值必然已经存在)。

从其父因子函数中,以取回的父值为条件,为 $v$ 采样一个值。

end

看着图 2.19因子图,我们从上到下(从祖先到后代)沿着箭头运行祖先采样,通过在给定图中各变量父节点的条件下,为每个变量采样一个值。如果一个变量是某个确定性因子子变量,那么我们只需从它父变量的取值来计算它的值即可。

于是,从顶端开始,我们从 Bernoulli(0.5) 分布为 skill 数组的每个元素采样一个值——换句话说,我们以 50% 的概率true,否则选 false。对于某道题的 relevantSkills 数组元素,我们只需抽出 skill 数组中与该题相关的、已采样的值。随后把这些值 AND 在一起,得到 hasSkills。图 2.21 给出了 skillhasSkills 数组的一组 22 个样本示例。要得到一个多行的数据集,我们只需对每一行重复整个采样过程。注意,对每一行,凡是需要相同技能(即同一颜色)的题目,hasSkills 总是相同的。

图 2.21a

(a) 采样得到的 skill 数组

图 2.21b

(b) 由采样 skill 数组计算出的 hasSkills 数组

图 2.21c

(c) 采样得到的 isCorrect 数组

图 2.21:用祖先采样创建的合成数据集。首先采样 skill 数组,然后由它计算出 hasSkills 数组。接着在给定 hasSkills 数组的条件下采样 isCorrect 数组,其效果是使它成为 hasSkills 的一个带噪版本。

我们模型中祖先采样的最后一步,需要在给定 isCorrect 的父元素 hasSkills 的条件下,采样它的每个元素。当 hasSkillstrue 时,我们从 Bernoulli(0.9) 采样;当 hasSkillsfalse 时,我们从 Bernoulli(0.2) 采样(依照表 2.1)。执行这一步的结果给出了图 2.21c 的 isCorrect 样本。注意,这些样本最终看起来像是 hasSkills 样本的带噪版本——大约每十个白色方块中就有一个被翻转为彩色,而大约每五个彩色方块中就有一个被翻转为白色。

我们现在有了一整个采样得到的数据集,可以在它上面运行我们的推断算法,来测试它是否正确工作。推断出的技能概率展示在图 2.22 中,紧挨着我们所采样的实际技能。与真实数据不同,这里的结果相当令人信服:推断出的技能看起来与实际采样的技能非常相似。所以,当我们在一个完全符合我们模型的数据集上运行推断时,结果是好的。这表明推断算法工作良好,问题必然出在我们的模型与真实数据不匹配上。

图 2.22a

(a) 推断出的技能

图 2.22b

(b) 采样得到的技能

图 2.22:从一个样本数据集推断出的技能,与该数据集实际采样的技能并列展示。推断出的技能接近实际技能,表明推断算法工作良好。

一个重要而微妙的点是:即使数据与模型完美匹配,推断出的技能也只是接近、而非完全等同于采样得到的技能。这是因为,即便给定了测试中的所有答案,技能上仍然残留着一些不确定性。例如,在个体不具备技能 1(Core)或技能 2(OOP)的情形下,技能 7(C#)的后验概率是不确定的。这说得通,因为 C# 技能只与前两项技能组合在一起被考察——如果一个人不具备它们,那么无论他是否懂 C#,都会答错相关题目。所以在这种情况下,推断算法对这个人是否具备 C# 技能感到不确定是正确的。我们可以利用这个信息来改进测试,例如添加一些直接单独考察 C# 技能的题目。

弄清模型出了什么问题

我们已经确定我们的模型假设与数据不匹配——现在我们需要识别是哪个(些)假设出了错。我们可以再次使用采样来做到这一点,但这次不是采样 skill 数组,而是把它设为真实(自评)值。如果我们随后采样 isCorrect 数组,它将向我们展示:如果人们拥有这些技能,模型预期他们会答错哪些题。通过把这与我们数据集中实际的 isCorrect 数组作比较,我们就能看出模型的假设在何处与现实不符。图 2.23 表明,实际的 isCorrect 数据看起来与采样数据相当不同。

图 2.23a

(a) 给定真实(自评)技能所采样的 isCorrect 数组

图 2.23b

(b) 我们数据集中观测到的 isCorrect 数组

图 2.23:建模问题可以通过比较来诊断:(a) 在给定自评技能的条件下从模型采样得到的 isCorrect 数据,与 (b) 观测到的 isCorrect 数据(显示志愿者实际答错了哪些题)。

最大的差异似乎在于:给定志愿者陈述的技能,他们答对的题目比我们模型所预测的要多得多。这表明他们能够猜对一道题的答案,其频率远高于我们模型所假设的五分之一。仔细想想,这说得通——即使某人不具备回答一道题的技能,他也可能凭借常识或聪明的猜测排除掉一些选项。

我们可以进一步研究这一点,方法是:计算在给定自评技能的条件下,模型预测志愿者答对每道题的比例,然后把它与他们实际答对的比例作比较(图 2.24)。

图 2.24

图 2.24:对于测试中的 48 道题,模型在给定自评技能下预测答对每道题的人数比例(蓝色),与实际答对的比例(红色)的对比。

对于少数几道题,答对的人数比例与模型预测的一致——但对于大多数题目,实际比例都高于预测比例。这表明有些题比其他题更容易猜,而且它们被猜对的频率可以高于五分之一。所以我们需要更改我们的假设(以及我们的模型),以允许不同题目有不同的猜测概率。我们可以把第四条假设修改如下:

  1. 如果一位应试者不具备一道题所需的全部技能,他将 随机挑选一个答案 猜一个答案,其中猜对的概率对大多数题目约为 20%,但对非常容易猜的题目可高达约 60%

这条假设意味着,我们不再对所有题目使用固定的猜测概率,而是需要扩展我们的模型,为每道题学习一个不同的猜测概率

本页引入概念回顾

祖先采样(ancestral sampling):一种从概率模型产生样本的过程:先用没有父节点的变量的先验分布对它们进行采样,然后在这些采样值的条件下采样它们的子变量,再类似地采样这些子变量的子变量,如此类推,直到图中所有变量都被赋值为止。

自我评估 2.5

以下练习将帮助你巩固本节所学的概念。在做题时,回顾正文或上面的概念小结可能会有所帮助。

  1. 列一份机器学习系统问题成因的检查清单(数据问题、模型问题或推断问题)。按你认为最可能发生的顺序对成因排序。那么,如果你将来在处理一个机器学习问题,这份检查清单在诊断问题的根本原因时可能会很有用。
  2. 编写一个程序来实现本节所述的技能模型中的祖先采样,并用它制作一个合成数据集。把这个数据集可视化,例如用你在上一个自我评估中开发的可视化。检查你的样本看起来是否与图 2.21 的样本相似。
  3. 试着更改我们在模型中选定的几个概率值,例如具备某项技能的先验概率,或猜对答案的概率。再次运行你的采样程序,看看合成数据集如何变化。你可以设想重复这一过程,直到在模型假设下合成数据尽可能像真实数据为止。这样做会相当低效,所以我们转而把这些概率值作为推断算法的一部分来学习,正如我们将在下一节看到的那样。

参考文献

[Bishop, 2006] Bishop, C. M. (2006). Pattern Recognition and Machine Learning. Springer.


下一节:学习猜测概率