按标识符拆分训练/测试集?

数据挖掘 机器学习 Python 数据集 数据清理 随机算法
2021-10-06 11:22:45

我知道 sklearn 必须train_test_split()拆分训练集和测试集。但我读到,即使设置了随机种子,如果您的实际数据集定期更新,随机种子将随着每个更新的数据集重置并采用不同的训练/测试拆分。这样做,您的 ML 算法最终将覆盖整个数据集,从而违背了训练/测试拆分的目的,因为随着时间的推移,它最终会在整个数据集的太多部分上进行训练。

我正在阅读的书(Hands-On Machine Learning with Scikit-Learn and Tensorflow)提供了此代码以按 id 拆分训练/测试:

# Function to check test set's identifier.
def test_set_check(identifier, test_ratio):
    return crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32

# Function to split train/test
def split_train_test_by_id(data, test_ratio, id_column):
    ids = data[id_column]
    in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio))
    return data.loc[~in_test_set], data.loc[in_test_set]

它说当没有给出 ID 列时,通过索引行或从变量之一创建唯一索引来创建一个。

我的问题是:

  1. 第三行在做什么:

    crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32

  2. 第 2 行到最后一行的匿名函数在做什么?

    lambda id_: test_set_check(id_, test_ratio)

  3. 在实践中,您通常以这种方式按 id 拆分数据集吗?

谢谢,

格雷格

3个回答

Auriel Geron 的书中,有对该方法的简短描述:

您可以计算每个实例标识符的哈希值,仅保留哈希值的最后一个字节,如果该值小于或等于 51(约 256 的 20%),则将该实例放入测试集中。这可确保测试集在多次运行中保持一致,即使您刷新数据集也是如此。新的测试集将包含 20% 的新实例,但不会包含之前在训练集中的任何实例。

虽然对确切发生的事情和原因的完整解释可能最好放在 StackOverflow 上,但我可以尝试回答您的问题,首先提供一些背景信息。

该方法使用循环冗余检查,这是一种检查原始内存块是否未损坏/更改的方法。这是一种确保数据完整性的方法,例如在网络流量中 - 检查消息在发送和接收之间是否发生了变化。

对于训练/测试拆分,它正在检查每个样本的唯一标识符。我们有一列为每个样本提供了一个 ID - 这永远不应该改变!不要删除行,只在末尾追加新的唯一 ID。

在这部分: test_ratio * 2**32, 部分232 表示 32 位系统的最大整数。

0xFFFFFFFF 是一个大数;这是十六进制表示2321

要回答您的问题:

  1. 第三行在做什么:

crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32

根据我上面提供的信息,我们看到该crc32函数在内存中找到校验和值(唯一标识符)。如果我们知道唯一 ID 从未改变,那么我们确保crc32(np.int64(identifier)) & 0xffffffff在所有 Python 版本和平台上始终返回完全相同的数值。

想象一下,我们为训练提供 0-80 范围内的 ID,为测试提供 81-100 范围内的 ID。不,我们要确保样本的 ID 位于第一个存储桶中。我们检查它的ID很简单,小于81,对吧?好吧,我们在上面制作的数值被检查为小于我们的test_ratio * 2**32,其中2**32是最大的 32 位数字。它检查样本的 ID 是否在训练数据范围内,而不是在 test bucket::> test_ratio * 2**32中。

  1. 第 2 行到最后一行的匿名函数在做什么?

lambda id_: test_set_check(id_, test_ratio)

这只是将我们的test_set_check函数应用于每个样本的唯一标识符。在 Pandas Series 对象上使用方法apply(这里是 Pandas DataFrame 的一列)。

  1. 在实践中,您通常以这种方式按 ID 拆分数据集吗?

不是真的... Scikit-Learntrain_test_split通常已经足够好了。我认为在过度担心随机拆分的影响之前,还有许多其他方法可以消除模型中的偏差和错误。

例如,窥探偏差,您在决定模型架构/管道之前自己分析整个数据集,从而结合整个分布的知识,这本质上是我们模型的偏差。

过度拟合也存在偏差,例如在顺序成像数据(想想视频帧)中,即使您可能想要检测的对象不是,背景也是一致的。您的模型将根据不稳健的背景了解预期内容!在这里,您可能会考虑使用地理分割(根本不是随机的)。


在旁注中,还有一种在 Python 中设置随机种子的稍微健壮的方法(而不是使用 NumPy 的随机种子生成器)。看看这里的一些差异


有用的资源:
  1. https://stackoverflow.com/questions/36819849/detect-int32-overflow-using-0xffffffff-masking-in-python
  2. https://pynative.com/python-random-module/
  3. https://stackoverflow.com/questions/30092226/how-to-calculate-crc32-with-python-to-match-online-results
  4. https://stackoverflow.com/questions/49331030/bitwise-xor-0xffffffff/49332291#49332291

@n1k31t4 已经提供了很好的答案,尽管我在阅读后仍有疑问。我解决了其余的问题并在这里分享。

对于第一个问题

第三行在做什么: crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32

我们首先忽略该部分& 0xffffffff,因为来自包的文档https://docs.python.org/3/library/zlib.html

在 3.0 版更改: 总是返回一个无符号值。要在所有 Python 版本和平台上生成相同的数值,请使用 adler32(data) & 0xffffffff。

因此,如果您使用的是 3.0 版本,您可以忽略这部分并且 crc32(np.int64(identifier)) < test_ratio * 2**32仍然可以使用。

要理解上述工作的原因,只需要将 crc32均匀分布在 unsigned int32 https://stackoverflow.com/questions/38315172/distribution-of-crc-checksums上,因此如果样本量足够大,那么test_ratio * sample_size 样本点的数量将小于 test_ratio *232.

如果你仍然好奇& 0xffffffff在做什么,它会将有符号的 int32 映射到无符号的 int32(负映射到正,而非负不变)。0xffffffff 是 0b11111111111111111111111111111111(三十二个)的十六进制表示。因此,如果您按位 & 使用它,您将得到以下结果:

>>> print(-1 & 0xFFFFFFFF)
4294967295
>>> print(-1 & 0b11111111111111111111111111111111)
4294967295

要了解按位运算,您需要查看 python 的二进制补码实现。

https://wiki.python.org/moin/BitwiseOperators#:~:text=Two's%20Complement%20binary%20for%20Negative%20Integers%3A&text=So%20%2D1%20is%20complement

如果数据具有索引列并且您插入或删除一行,则更改该行后面的所有数据(当您更改它们的索引时)。因此,它们计算的唯一标识符train_test_split会发生变化,并且在数据更新后拆分将不一致。解决此问题的一种方法是将新数据附加到旧数据中,并且永远不要删除记录(如《Hands-On Machine Learning with Scikit... 》一书中所述)。不要忘记,我们假设随机数生成器是种子。

crc32(np.int64(identifier)) = 从给定值创建哈希

crc32(np.int64(identifier)) & 0xffffffff= 确保哈希值不超过 2^32(或 4294967296)。检查以下简化示例以获得更好的理解。

18 & 0xf = ?

18 in binary 0b10010

0xf in binary 0b1111 为了使其与前一个数字的长度相同,我们在左侧附加一个 0。所以0xf in binary 0b01111

现在,我们可以进行按位and运算了0b10010 & 0b01111 = 0b00010 = 3因此& 0xf确保输出永远不会超过 15。现在,反思原始代码以正确理解它。

crc32(np.int64(identifier)) & 0xffffffff < test_ratio * 2**32. 此行返回TrueFalse设为test_ratio0.2。然后,任何小于 0.2 * 4294967296 的哈希值都会返回True,并将被添加到测试集中;否则,它False会返回并添加到训练集中。