我正在使用sklearn的决策树来替代一个混乱且难以维护的业务规则实现,这些规则原本是一个冗长的if-elif-else链。我通过数千个测试用例来验证树的正确性,涵盖了所有标签,但有时用作训练数据的规则表会出现错误,导致一些测试随机失败。
除了针对结果的测试用例外,我需要一种验证树的方法。假设所有叶节点的gini值为0.0,是否可以认为在使用不同随机种子生成的树之间不会有分类上的随机变化?如果我需要在我的应用中强制执行这一点,在更新训练数据时检查这一点是否合理?
请注意,我的案例并不是一个典型的分类问题,因为我已经在代码中实现了决策树,我希望使用算法从精心定制的训练数据中生成一个等效的树,而不是使用真实世界的数据样本,原因是维护包含业务规则的数据集比维护代码更容易。
因此,在我的数据集中,特征理想情况下应覆盖所有可能的值范围,并为这些值提供一个明确的标签。例如,虽然真实世界的训练集可能是这样的:
features = [[1], [1.1], [2], [2.3]]labels = ['sativa', 'sativa', 'indica', 'indica']
算法可能会随机生成一个像这样的树1:
if feature < 1.75: return 'sativa'else: return 'indica'
以及一个像这样的树2:
if feature < 1.55: return 'sativa'else: return 'indica'
然而,我的训练集不会有随机性发生的间隙。它会是这样的:
features = [[1], [1.9], [2], [2.3]]labels = ['sativa', 'sativa', 'indica', 'indica']
因此,无论初始随机状态如何,树总是会是(显然,忽略0.1以下的差异):
if feature < 1.95: return 'sativa'else: return 'indica'
我的问题正是需要验证训练集中是否存在错误,以及是否存在可能发生随机变化的值间隙,或者是否同一组特征被分配了不同的标签。固定随机状态并不能解决这个问题,它只能保证相同的数据总是生成相同的树。
那么,除了在生成树之前验证数据中的这些问题,或者多次运行全面的测试以排除随机变化之外,还有没有其他方法可以确定这种情况是否发生?
回答:
由于您拥有标签规则,并且知道特征的可能范围,您可以实现您的目标。
让我们通过示例来可视化这一点
Gini指数
Gini指数是什么意思?它试图将训练集拟合到您刚创建的树中,并告诉您它们的项目会被错误标记的程度。因此,在树1和树2中,gini值为0,因为训练集中的每个示例都会被正确标记。
树1和树2有相同的训练集,且gini值为0.0。但是,如果我们尝试标记x=[1.7],我们会得到不同的结果。
解决方案
由于您了解将特征集与其相应的标签绑定在一起的规则,因此有可能确保给定一个训练集,它将始终生成一个树,对于任何可能的特征,都会输出正确的结果,如果满足以下条件:
- 连续值的特征在某个范围内,例如[2,30]
- 您可以设定一个精度阈值,例如上面的示例至少以0.1的步长变化
- 您可以为每种可能的组合生成示例
- 您的树的gini值为0.0
(基本上,我们是在说,由于gini值为0.0,那么如果您输入的是训练集中的数据,它将被正确标记。这里没有巨大的跳跃性结论)。
因此,如果:
feature1 是 numpy.linspace(1,2,11) 中的一个 = [1., 1.1, 1.2, 1.3, ..., 2] feature2 是 True 或 False 中的一个
并且:
您的示例包含所有可能的线性组合 您所有节点的gini值之和为0 未来的示例在训练集的条件边界内
那么您可以确定:
您覆盖了所有可能的示例 所有可能的示例都被正确标记
这样,随机初始化的随机树将具有相同的输出。它们只会在训练集范围之外的示例,或者那些值不遵循阈值规则的示例上有所不同。
猜您已经这样做了,但使用测试覆盖工具创建训练集是个好主意,以确保所有可能的示例都被创建。
关于在连续值中使用所有可能值的必要性
我在声明您需要为连续值拥有所有可能的值时格外小心,比如[1.0, 1.1, 1.2, … 1.9, 2.0]。如果特征只在一个节点中使用,那么您可以只使用边界值(示例中的1.9和2.0)。但是,如果您的if-else结构更复杂,我们可能会遇到一些不可预测的复杂场景。在评估特征f1的节点之后,我们可能会在左侧生成的节点上有一个条件(如if f2 > 5),以及在右侧节点上有一个条件(如if f2 < 3),或者类似的情况。您可能会得到错误的结果。
如果您的组合变得太大,那么将特征二值化可能是一个好主意。如果您有一个连续特征,如:
if f1 > 3: f1 = 'many' if 3 <= f1 < 0 = 'little'
您可以将其转换为(1 0)和(0 1),使用DictVectorizer对象。
虽然您增加了维度,但您的决策树只会为这两个特征创建一个节点。如果它只检查第一个是否为真,第二个就是多余的,因为它们是互斥的。
这也是如果您的数据没有边界时的一个解决方案。如果您有一个没有范围的x特征,您可以将其二值化为(x < 0, 0<= x < 5, 5 <= x),将一个连续值转换为(1 0 0), (0 1 0)或(0 0 1),使您能够评估所有可能的组合。
这样,您可以大幅减少组合集的大小。
希望这对您有帮助。祝好运!